Files
reui-blocks/blocks/navbar24.tsx
T

305 lines
7.3 KiB
TypeScript

"use client";
import {
Bell,
Heart,
Home,
LogOut,
PanelBottomOpen,
Settings,
ShoppingCart,
User,
} from "lucide-react";
import type { MotionValue } from "motion/react";
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
useTransform,
} from "motion/react";
import { useRef, useState } from "react";
import * as React from "react";
import { cn } from "@/lib/utils";
interface Navbar24Props {
/** When true, positions inside the block preview instead of the viewport. */
contained?: boolean;
className?: string;
}
const Navbar24 = ({ contained = false, className }: Navbar24Props) => {
const dockPosition = contained ? "absolute" : "fixed";
const tabs = [
{
title: "خانه",
icon: <Home />,
href: "#home",
},
{
title: "اعلان‌ها",
icon: <Bell />,
href: "#notifications",
},
{
title: "پروفایل",
icon: <User />,
href: "#profile",
},
{
title: "تنظیمات",
icon: <Settings />,
href: "#settings",
},
{
title: "نشان‌شده‌ها",
icon: <Heart />,
href: "#favorites",
},
{
title: "پلن‌ها",
icon: <ShoppingCart />,
href: "#cart",
},
{
title: "خروج",
icon: <LogOut />,
href: "#logout",
},
];
return (
<section
dir="rtl"
lang="fa"
className={cn(
"relative overflow-visible",
contained ? "py-8" : "py-32",
className,
)}
>
<div className="container flex justify-center">
<FloatingDock
contained={contained}
desktopClassName={cn(
dockPosition,
"bottom-4 left-1/2 -translate-x-1/2",
)}
mobileClassName={cn(dockPosition, "right-6 bottom-6")}
items={tabs}
/>
</div>
</section>
);
};
export { Navbar24 };
// Below is the modified component from Aceternity UI
// Original source: npx shadcn@latest add https://ui.aceternity.com/registry/floating-dock.json
// Modified to follow our coding standards and design system
// We respect copyright and attribution to the original creators
const FloatingDock = ({
items,
desktopClassName,
mobileClassName,
contained = false,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
desktopClassName?: string;
mobileClassName?: string;
contained?: boolean;
}) => {
return (
<>
<FloatingDockDesktop items={items} className={desktopClassName} />
<FloatingDockMobile
items={items}
className={cn(mobileClassName, contained && "relative")}
/>
</>
);
};
const FloatingDockMobile = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const [open, setOpen] = useState(false);
return (
<div className={cn("block md:hidden", className)}>
<AnimatePresence>
{open && (
<motion.div
layoutId="nav"
className="absolute inset-x-0 bottom-full z-99 mb-2 flex flex-col gap-2"
>
{items.map((item, idx) => (
<motion.div
key={item.title}
initial={{ opacity: 0, y: 10 }}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 10,
transition: {
delay: idx * 0.05,
},
}}
transition={{ delay: (items.length - 1 - idx) * 0.05 }}
>
<a
href={item.href}
key={item.title}
className="flex h-10 w-10 items-center justify-center rounded-xl bg-foreground text-muted-foreground"
>
<div>{item.icon}</div>
</a>
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
<button
type="button"
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-label={open ? "بستن منو" : "باز کردن منو"}
className="flex h-10 w-10 items-center justify-center rounded-xl bg-foreground text-muted-foreground"
>
<PanelBottomOpen />
</button>
</div>
);
};
const FloatingDockDesktop = ({
items,
className,
}: {
items: { title: string; icon: React.ReactNode; href: string }[];
className?: string;
}) => {
const mouseX = useMotionValue(Infinity);
return (
<motion.div
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
className={cn(
"z-99 mx-auto hidden h-16 items-center justify-center gap-2 rounded-3xl bg-foreground px-5 md:flex",
className,
)}
>
{items.map((item) => (
<IconContainer mouseX={mouseX} key={item.title} {...item} />
))}
</motion.div>
);
};
function IconContainer({
mouseX,
title,
icon,
href,
}: {
mouseX: MotionValue;
title: string;
icon: React.ReactNode;
href: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const distance = useTransform(mouseX, (val) => {
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
return val - bounds.x - bounds.width / 2;
});
const widthTransform = useTransform(distance, [-150, 0, 150], [40, 60, 40]);
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 60, 40]);
const widthTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);
const heightTransformIcon = useTransform(
distance,
[-150, 0, 150],
[20, 40, 20],
);
const width = useSpring(widthTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const height = useSpring(heightTransform, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const widthIcon = useSpring(widthTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const heightIcon = useSpring(heightTransformIcon, {
mass: 0.1,
stiffness: 150,
damping: 12,
});
const rounded = useSpring(
useTransform(distance, [-150, 0, 150], [50, 10, 50]),
{
mass: 0.1,
stiffness: 150,
damping: 12,
},
);
const [hovered, setHovered] = useState(false);
return (
<a href={href}>
<motion.div
ref={ref}
style={{ width, height, borderRadius: rounded }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className="relative flex aspect-square items-center justify-center bg-background/10"
>
<AnimatePresence>
{hovered && (
<motion.div
initial={{ opacity: 0, y: 10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: 2, x: "-50%" }}
className="absolute -top-8 left-1/2 w-fit rounded-full bg-muted px-4 py-0.5 text-xs whitespace-pre"
>
{title}
</motion.div>
)}
</AnimatePresence>
<motion.div
style={{ width: widthIcon, height: heightIcon }}
className="flex items-center justify-center text-muted-foreground"
>
{icon}
</motion.div>
</motion.div>
</a>
);
}