318 lines
8.2 KiB
TypeScript
318 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { IconMenu2, IconX } from "@tabler/icons-react";
|
|
import {
|
|
motion,
|
|
AnimatePresence,
|
|
useScroll,
|
|
useMotionValueEvent,
|
|
} from "motion/react";
|
|
import React, { useRef, useState } from "react";
|
|
|
|
interface NavbarProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const Navbar = ({ children, className }: NavbarProps) => {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const { scrollY } = useScroll({
|
|
target: ref,
|
|
offset: ["start start", "end start"],
|
|
});
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
useMotionValueEvent(scrollY, "change", (latest) => {
|
|
if (latest > 100) {
|
|
setVisible(true);
|
|
} else {
|
|
setVisible(false);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
className={cn("fixed inset-x-0 top-0 z-40 w-full", className)}
|
|
>
|
|
{React.Children.map(children, (child) =>
|
|
React.isValidElement(child)
|
|
? React.cloneElement(child, { visible } as { visible: boolean })
|
|
: child
|
|
)}
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
interface NavBodyProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
visible?: boolean;
|
|
}
|
|
|
|
export const NavBody = ({ children, className, visible }: NavBodyProps) => {
|
|
return (
|
|
<motion.div
|
|
animate={{
|
|
backdropFilter: visible ? "blur(10px)" : "none",
|
|
boxShadow: visible
|
|
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
|
|
: "none",
|
|
width: visible ? "40%" : "100%",
|
|
y: visible ? 20 : 0,
|
|
}}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 200,
|
|
damping: 50,
|
|
}}
|
|
style={{
|
|
minWidth: "800px",
|
|
}}
|
|
className={cn(
|
|
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
|
|
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
|
|
visible && "bg-black/90",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
interface NavItem {
|
|
name: string;
|
|
link: string;
|
|
}
|
|
|
|
interface NavItemsProps {
|
|
items: NavItem[];
|
|
className?: string;
|
|
onItemClick?: () => void;
|
|
}
|
|
|
|
export const NavItems = ({
|
|
items,
|
|
className,
|
|
onItemClick,
|
|
}: NavItemsProps) => {
|
|
const [hovered, setHovered] = useState<number | null>(null);
|
|
|
|
return (
|
|
<motion.div
|
|
onMouseLeave={() => setHovered(null)}
|
|
className={cn(
|
|
"absolute inset-0 hidden flex-1 flex-row items-center justify-center space-x-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:space-x-2",
|
|
className
|
|
)}
|
|
>
|
|
{items.map((item, idx) => (
|
|
<a
|
|
onMouseEnter={() => setHovered(idx)}
|
|
onClick={onItemClick}
|
|
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
|
|
key={`link-${idx}`}
|
|
href={item.link}
|
|
>
|
|
{hovered === idx && (
|
|
<motion.div
|
|
layoutId="hovered"
|
|
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
|
|
/>
|
|
)}
|
|
<span className="relative z-20">{item.name}</span>
|
|
</a>
|
|
))}
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
interface MobileNavProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
visible?: boolean;
|
|
}
|
|
|
|
export const MobileNav = ({
|
|
children,
|
|
className,
|
|
visible,
|
|
}: MobileNavProps) => {
|
|
return (
|
|
<motion.div
|
|
animate={{
|
|
backdropFilter: visible ? "blur(10px)" : "none",
|
|
boxShadow: visible
|
|
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
|
|
: "none",
|
|
width: visible ? "90%" : "100%",
|
|
paddingRight: visible ? "12px" : "0px",
|
|
paddingLeft: visible ? "12px" : "0px",
|
|
borderRadius: visible ? "4px" : "2rem",
|
|
y: visible ? 20 : 0,
|
|
}}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 200,
|
|
damping: 50,
|
|
}}
|
|
className={cn(
|
|
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
|
|
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
|
|
visible && "bg-black/90",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
interface MobileNavHeaderProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const MobileNavHeader = ({
|
|
children,
|
|
className,
|
|
}: MobileNavHeaderProps) => {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex w-full flex-row items-center justify-between",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface MobileNavMenuProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const MobileNavMenu = ({
|
|
children,
|
|
className,
|
|
isOpen,
|
|
}: MobileNavMenuProps) => {
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className={cn(
|
|
"absolute inset-x-0 top-16 z-50 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white px-4 py-8 shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset] dark:bg-neutral-950",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
};
|
|
|
|
interface MobileNavToggleProps {
|
|
isOpen: boolean;
|
|
onClick: () => void;
|
|
}
|
|
|
|
export const MobileNavToggle = ({ isOpen, onClick }: MobileNavToggleProps) => {
|
|
return isOpen ? (
|
|
<IconX
|
|
className="h-6 w-6 cursor-pointer text-black dark:text-white"
|
|
onClick={onClick}
|
|
/>
|
|
) : (
|
|
<IconMenu2
|
|
className="h-6 w-6 cursor-pointer text-black dark:text-white"
|
|
onClick={onClick}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface NavbarLogoProps {
|
|
href?: string;
|
|
logoSrc?: string;
|
|
logoAlt?: string;
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const NavbarLogo = ({
|
|
href = "#",
|
|
logoSrc,
|
|
logoAlt = "Logo",
|
|
children,
|
|
className,
|
|
}: NavbarLogoProps) => {
|
|
return (
|
|
<a
|
|
href={href}
|
|
className={cn(
|
|
"relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-black dark:text-white",
|
|
className
|
|
)}
|
|
>
|
|
{logoSrc ? (
|
|
<img src={logoSrc} alt={logoAlt} width={30} height={30} />
|
|
) : null}
|
|
{children}
|
|
</a>
|
|
);
|
|
};
|
|
|
|
interface NavbarButtonProps
|
|
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
href?: string;
|
|
as?: "a" | "button";
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
variant?: "primary" | "secondary" | "dark" | "gradient";
|
|
}
|
|
|
|
export const NavbarButton = ({
|
|
href,
|
|
as: Tag = "a",
|
|
children,
|
|
className,
|
|
variant = "primary",
|
|
...props
|
|
}: NavbarButtonProps) => {
|
|
const baseStyles =
|
|
"px-4 py-2 rounded-full text-sm font-bold relative cursor-pointer transition duration-200 inline-block text-center";
|
|
|
|
const variantStyles = {
|
|
primary:
|
|
"bg-[hsl(198,93%,42%)] text-white border border-white/20 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),inset_0_2px_2px_rgba(255,255,255,0.2),0_2px_4px_rgba(0,0,0,0.2),0_4px_8px_rgba(0,0,0,0.15)] hover:border-white/40 hover:shadow-[inset_0_1px_1px_rgba(255,255,255,0.3),inset_0_2px_2px_rgba(255,255,255,0.25),0_2px_4px_rgba(0,0,0,0.2),0_4px_8px_rgba(0,0,0,0.15)]",
|
|
secondary: "bg-transparent shadow-none dark:text-white",
|
|
dark: "btn !text-white",
|
|
gradient:
|
|
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
|
|
};
|
|
|
|
const componentProps =
|
|
Tag === "a"
|
|
? { href: href ?? undefined, ...props }
|
|
: { ...props };
|
|
|
|
return (
|
|
<Tag
|
|
className={cn(baseStyles, variantStyles[variant], className)}
|
|
{...(componentProps as React.ComponentProps<typeof Tag>)}
|
|
>
|
|
{children}
|
|
</Tag>
|
|
);
|
|
};
|