"use client"; import { useCallback, useEffect, useId, useRef, useState, type CSSProperties, type ReactNode, } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { ArrowUpRight, ChevronDown, Code2, Menu, X } from "lucide-react"; import { cn } from "@/lib/utils"; gsap.registerPlugin(useGSAP); export interface FullscreenNavbarLink { label: string; href: string; } export interface FullscreenNavbarItem { label: string; href?: string; links?: readonly FullscreenNavbarLink[]; } export interface FullscreenNavbarSpotlight { title: string; description: string; tag?: string; image: string; href: string; } export interface FullscreenNavbarProps { brand?: string; brandHref?: string; logo?: ReactNode; items?: readonly FullscreenNavbarItem[]; connectLinks?: readonly FullscreenNavbarLink[]; spotlight?: FullscreenNavbarSpotlight; actionLabel?: string; actionHref?: string; footerText?: string; contactLabel?: string; contactHref?: string; duration?: number; stiffness?: number; damping?: number; stagger?: number; background?: string; foreground?: string; accent?: string; headerForeground?: string; hoverDropdowns?: boolean; contained?: boolean; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; className?: string; } export const FULLSCREEN_NAVBAR_ITEMS: readonly FullscreenNavbarItem[] = [ { label: "Components", links: [ { label: "Explore the library", href: "/components" }, { label: "Animated buttons", href: "/components/animated-button" }, { label: "Navigation bars", href: "/components/curved-navbar" }, { label: "Image loaders", href: "/components/grid-image-loader" }, ], }, { label: "Documentation", links: [ { label: "Introduction", href: "/docs/introduction" }, { label: "Installation", href: "/docs/installation" }, { label: "Theming", href: "/docs/theming" }, { label: "CLI & registry", href: "/docs/cli" }, ], }, { label: "Community", links: [ { label: "Dev Club on GitHub", href: "https://github.com/devclub-nstru/component_library", }, { label: "Report an issue", href: "https://github.com/devclub-nstru/component_library/issues", }, { label: "Contribute", href: "https://github.com/devclub-nstru/component_library/blob/main/CONTRIBUTING.md", }, ], }, { label: "Showcase", href: "/components" }, { label: "Our story", href: "/docs/introduction" }, { label: "Source code", href: "https://github.com/devclub-nstru/component_library", }, ]; export const FULLSCREEN_NAVBAR_CONNECT_LINKS: readonly FullscreenNavbarLink[] = [ { label: "GitHub", href: "https://github.com/devclub-nstru/component_library", }, { label: "Docs", href: "/docs" }, { label: "Dev Club", href: "https://devclub.co" }, ]; const bounded = (value: number, min: number, max: number, fallback: number) => Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback; function springEase(stiffness: number, damping: number, duration: number) { const frequency = Math.sqrt(stiffness); const ratio = damping / (2 * frequency); const response = (time: number) => { if (ratio < 1) { const rate = frequency * Math.sqrt(1 - ratio * ratio); return ( 1 - Math.exp((-damping * time) / 2) * (Math.cos(rate * time) + (damping / (2 * rate)) * Math.sin(rate * time)) ); } if (Math.abs(ratio - 1) < 0.001) return 1 - (1 + frequency * time) * Math.exp(-frequency * time); const delta = Math.sqrt(ratio * ratio - 1); const slow = -frequency * (ratio - delta); const fast = -frequency * (ratio + delta); return ( 1 - (fast * Math.exp(slow * time) - slow * Math.exp(fast * time)) / (fast - slow) ); }; const end = response(duration); return (progress: number) => progress === 1 ? 1 : response(progress * duration) / Math.max(0.001, end); } function NavbarHeader({ brand, brandHref, logo, actionLabel, actionHref, menuButton, onNavigate, }: { brand: string; brandHref: string; logo?: ReactNode; actionLabel: string; actionHref: string; menuButton: ReactNode; onNavigate?: () => void; }) { return (
{brand}
{actionLabel} {menuButton}
); } const HOVER_CLICK_GRACE_MS = 350; const menuButtonClass = "flex min-h-10 min-w-24 cursor-pointer items-center justify-center gap-2 rounded-full border border-current/15 bg-current/5 px-4 text-xs font-medium backdrop-blur-lg hover:bg-current/10 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--fullnav-accent)]"; function NavbarRow({ item, index, expanded, onExpand, onNavigate, mobile, duration, stiffness, damping, hoverDropdowns, }: { item: FullscreenNavbarItem; index: number; expanded: boolean; onExpand: () => void; onNavigate: () => void; mobile: boolean; duration: number; stiffness: number; damping: number; hoverDropdowns: boolean; }) { const rootRef = useRef(null); const panelRef = useRef(null); const contentRef = useRef(null); const arrowRef = useRef(null); const animateRef = useRef<((next: boolean) => void) | null>(null); const expandedRef = useRef(expanded); const hoverOpenedAtRef = useRef(0); const id = useId(); const hasLinks = Boolean(item.links?.length); useEffect(() => { expandedRef.current = expanded; }, [expanded]); useGSAP( () => { const panel = panelRef.current; const content = contentRef.current; const arrow = arrowRef.current; if (!panel || !content || !arrow) return; const media = gsap.matchMedia(); media.add( { reduced: "(prefers-reduced-motion: reduce)", all: "all" }, (context) => { const reduced = Boolean(context.conditions?.reduced); const animate = context.add("animate", (next: boolean) => { gsap.killTweensOf([panel, arrow, content]); if (reduced) { gsap.set(panel, { height: next ? "auto" : 0, autoAlpha: next ? 1 : 0, }); gsap.set(arrow, { rotation: next ? 180 : 0 }); gsap.set(content, { y: 0, opacity: 1 }); return; } gsap.to(panel, { height: next ? "auto" : 0, autoAlpha: next ? 1 : 0, duration: duration * 0.55, ease: "power3.inOut", overwrite: "auto", }); gsap.to(arrow, { rotation: next ? 180 : 0, duration: duration * 0.65, ease: springEase(stiffness, damping, duration * 0.65), overwrite: "auto", }); gsap.fromTo( content, { y: next ? 8 : 0, opacity: next ? 0 : 1 }, { y: 0, opacity: next ? 1 : 0, duration: duration * 0.45, ease: "power3.out", }, ); }); animateRef.current = (next) => animate(next); animate(expandedRef.current); return () => { animateRef.current = null; }; }, ); return () => media.revert(); }, { scope: rootRef, dependencies: [duration, stiffness, damping, item.links], revertOnUpdate: true, }, ); useGSAP( () => { animateRef.current?.(expanded); }, { scope: rootRef, dependencies: [expanded] }, ); const label = ( <> {item.label} {hasLinks ? (