This commit is contained in:
2026-02-01 22:34:47 +01:00
parent 6228945065
commit 1d4584e5d9
7 changed files with 2370 additions and 210 deletions

View File

@@ -0,0 +1,317 @@
"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-md bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";
const variantStyles = {
primary:
"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]",
secondary: "bg-transparent shadow-none dark:text-white",
dark: "bg-black text-white 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]",
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>
);
};