{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"navigation-menu","type":"registry:block","title":"Navigation Menu","description":"Site navbar with a logo slot, plain links and mega-menu panels: a hover pill glides between items, the dropdown viewport morphs its size to each panel while content slides in the direction you move, panels open on hover intent or click/Enter, close on Escape or outside click, support arrow-key navigation, and collapse below md into a hamburger sheet with accordion sections.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/navigation-menu.tsx","type":"registry:component","target":"@components/motion/navigation-menu.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/navigation-menu\n\nimport { ChevronDown, Menu, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { SPRING_LAYOUT, SPRING_PANEL, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------------ */\n/* Types                                                                     */\n/* ------------------------------------------------------------------------ */\n\nexport type NavigationMenuLinkRenderProps = {\n  href: string;\n  className: string;\n  children: ReactNode;\n  onClick?: () => void;\n};\n\ntype RenderLink = (props: NavigationMenuLinkRenderProps) => ReactElement;\n\nexport type NavigationMenuLink = {\n  label: ReactNode;\n  href: string;\n  description?: ReactNode;\n  icon?: ReactNode;\n  /** Route through your router: `(p) => <Link {...p} />`. */\n  render?: RenderLink;\n};\n\nexport type NavigationMenuSection = {\n  title?: ReactNode;\n  links: NavigationMenuLink[];\n};\n\nexport type NavigationMenuItem =\n  | {\n      id: string;\n      label: ReactNode;\n      /** A plain top-level link. */\n      href: string;\n      render?: RenderLink;\n    }\n  | {\n      id: string;\n      label: ReactNode;\n      /** Link columns. Also feed the mobile accordion. */\n      sections: NavigationMenuSection[];\n      /** Extra column on desktop, e.g. a featured card. */\n      featured?: ReactNode;\n      /** Replace the desktop panel body entirely (mobile still uses `sections`). */\n      content?: ReactNode;\n    };\n\nexport interface NavigationMenuProps {\n  items: NavigationMenuItem[];\n  /** Brand slot on the left of the bar. */\n  logo?: ReactNode;\n  /** Right-hand slot (CTA buttons); hidden below `md`, repeated in the sheet. */\n  actions?: ReactNode;\n  /** Open panel id, `\"\"` when closed. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Hover-intent delay before the first panel opens (ms). */\n  openDelay?: number;\n  /** Grace period after the pointer leaves before closing (ms). */\n  closeDelay?: number;\n  \"aria-label\"?: string;\n  className?: string;\n}\n\ntype Size = { w: number; h: number };\ntype Geom = Size & { x: number };\n\nconst SLIDE = 28;\n\nconst panelVariants = {\n  enter: ({ dir, reduce }: { dir: number; reduce: boolean }) => ({\n    opacity: 0,\n    x: reduce ? 0 : dir * SLIDE,\n  }),\n  center: { opacity: 1, x: 0 },\n  exit: ({ dir, reduce }: { dir: number; reduce: boolean }) => ({\n    opacity: 0,\n    x: reduce ? 0 : dir * -SLIDE,\n    transition: { duration: 0.14 },\n  }),\n};\n\nconst linkRowClass =\n  \"group/link flex items-start gap-3 rounded-xl p-2.5 outline-none transition-colors hover:bg-muted focus-visible:bg-muted\";\n\nfunction defaultLink({ href, className, children, onClick }: NavigationMenuLinkRenderProps) {\n  return (\n    <a href={href} className={className} onClick={onClick}>\n      {children}\n    </a>\n  );\n}\n\nconst sectionKey = (section: NavigationMenuSection) =>\n  section.links.map((l) => l.href).join(\"|\");\n\nfunction hasPanel(\n  item: NavigationMenuItem,\n): item is Extract<NavigationMenuItem, { sections: NavigationMenuSection[] }> {\n  return \"sections\" in item;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Panel body                                                                */\n/* ------------------------------------------------------------------------ */\n\nfunction LinkRow({ link, onNavigate }: { link: NavigationMenuLink; onNavigate: () => void }) {\n  return (link.render ?? defaultLink)({\n    href: link.href,\n    className: linkRowClass,\n    onClick: onNavigate,\n    children: (\n      <>\n        {link.icon ? (\n          <span\n            aria-hidden\n            className=\"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-lg border border-border bg-background text-muted-foreground transition-colors group-hover/link:text-foreground group-focus-visible/link:text-foreground [&>svg]:size-4\"\n          >\n            {link.icon}\n          </span>\n        ) : null}\n        <span className=\"flex min-w-0 flex-col gap-0.5\">\n          <span className=\"text-sm font-medium text-foreground\">{link.label}</span>\n          {link.description ? (\n            <span className=\"text-xs leading-snug text-muted-foreground\">{link.description}</span>\n          ) : null}\n        </span>\n      </>\n    ),\n  });\n}\n\nfunction PanelBody({\n  item,\n  onNavigate,\n}: {\n  item: Extract<NavigationMenuItem, { sections: NavigationMenuSection[] }>;\n  onNavigate: () => void;\n}) {\n  if (item.content) return <>{item.content}</>;\n  return (\n    <div className=\"flex gap-2 p-2\">\n      {item.sections.map((section) => (\n        <div key={sectionKey(section)} className=\"flex w-60 flex-col gap-0.5\">\n          {section.title ? (\n            <p className=\"px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">\n              {section.title}\n            </p>\n          ) : null}\n          <ul className=\"flex flex-col gap-0.5\">\n            {section.links.map((link) => (\n              <li key={link.href}>\n                <LinkRow link={link} onNavigate={onNavigate} />\n              </li>\n            ))}\n          </ul>\n        </div>\n      ))}\n      {item.featured ? <div className=\"flex w-56 shrink-0\">{item.featured}</div> : null}\n    </div>\n  );\n}\n\n/** Reports its natural (untransformed) size so the viewport can morph to it. */\nfunction Measured({ onSize, children }: { onSize: (size: Size) => void; children: ReactNode }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const report = () => onSize({ w: el.offsetWidth, h: el.offsetHeight });\n    report();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(report);\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [onSize]);\n  return (\n    <div ref={ref} className=\"w-max\">\n      {children}\n    </div>\n  );\n}\n\nconst focusables = (root: HTMLElement | null) =>\n  Array.from(\n    root?.querySelectorAll<HTMLElement>(\"a[href],button:not([disabled]),[tabindex]:not([tabindex='-1'])\") ?? [],\n  );\n\n/* ------------------------------------------------------------------------ */\n/* NavigationMenu                                                            */\n/* ------------------------------------------------------------------------ */\n\nexport function NavigationMenu({\n  items,\n  logo,\n  actions,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  openDelay = 150,\n  closeDelay = 250,\n  \"aria-label\": ariaLabel = \"Main\",\n  className,\n}: NavigationMenuProps) {\n  const reduce = useReducedMotion() ?? false;\n  const uid = useId();\n  const [internal, setInternal] = useState(defaultValue);\n  const controlled = value !== undefined;\n  const open = controlled ? value : internal;\n\n  const [hovered, setHovered] = useState<string | null>(null);\n  const [dir, setDir] = useState(1);\n  const [geom, setGeom] = useState<Geom | null>(null);\n  const [mobileOpen, setMobileOpen] = useState(false);\n\n  const navRef = useRef<HTMLElement>(null);\n  const listRef = useRef<HTMLUListElement>(null);\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const actionsRef = useRef<HTMLDivElement>(null);\n  const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n  const openRef = useRef(open);\n  openRef.current = open;\n  const sizeRef = useRef<Size | null>(null);\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const openedBy = useRef<\"hover\" | \"click\">(\"click\");\n  const focusPanelOnOpen = useRef(false);\n  // The first frame after opening snaps to size; later switches morph.\n  const snap = useRef(true);\n\n  const panelIds = items.filter(hasPanel).map((i) => i.id);\n  const panelIdsRef = useRef(panelIds);\n  panelIdsRef.current = panelIds;\n  const panelId = (id: string) => `${uid}-panel-${id}`;\n\n  const setOpen = useCallback(\n    (next: string) => {\n      if (next === openRef.current) return;\n      const from = panelIdsRef.current.indexOf(openRef.current);\n      const to = panelIdsRef.current.indexOf(next);\n      if (from >= 0 && to >= 0) setDir(to > from ? 1 : -1);\n      if (!controlled) setInternal(next);\n      onValueChange?.(next);\n    },\n    [controlled, onValueChange],\n  );\n\n  const clearTimer = () => {\n    if (timer.current) clearTimeout(timer.current);\n    timer.current = null;\n  };\n  useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current);\n    },\n    [],\n  );\n\n  const schedule = (fn: () => void, ms: number) => {\n    clearTimer();\n    timer.current = setTimeout(fn, ms);\n  };\n\n  /* -- geometry ---------------------------------------------------------- */\n\n  const place = useCallback((size: Size) => {\n    const nav = navRef.current;\n    const trigger = triggerRefs.current[openRef.current];\n    if (!nav || !trigger) return;\n    const navRect = nav.getBoundingClientRect();\n    const tRect = trigger.getBoundingClientRect();\n    const max = Math.max(0, navRect.width - size.w);\n    const x = Math.min(Math.max(0, tRect.left - navRect.left - 8), max);\n    setGeom((g) => (g && g.x === x && g.w === size.w && g.h === size.h ? g : { x, ...size }));\n  }, []);\n\n  // Last measured size per panel. A panel revived mid-exit (A → B → A fast)\n  // keeps its mounted node and never re-measures, so reuse its cached size.\n  const sizes = useRef<Record<string, Size>>({});\n  const onPanelSize = useCallback(\n    (id: string) => (size: Size) => {\n      sizes.current[id] = size;\n      if (id !== openRef.current) return; // an exiting panel; ignore it\n      sizeRef.current = size;\n      place(size);\n    },\n    [place],\n  );\n  useLayoutEffect(() => {\n    const cached = open ? sizes.current[open] : undefined;\n    if (cached) {\n      sizeRef.current = cached;\n      place(cached);\n    }\n  }, [open, place]);\n  // Stable per-id callbacks so Measured's effect does not re-subscribe.\n  const sizeCallbacks = useRef<Record<string, (size: Size) => void>>({});\n  const sizeCallback = (id: string) => {\n    sizeCallbacks.current[id] ??= onPanelSize(id);\n    return sizeCallbacks.current[id];\n  };\n\n  useEffect(() => {\n    snap.current = geom === null;\n  }, [geom]);\n\n  useEffect(() => {\n    if (!open) {\n      setGeom(null);\n      sizeRef.current = null;\n    }\n  }, [open]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onResize = () => sizeRef.current && place(sizeRef.current);\n    window.addEventListener(\"resize\", onResize);\n    return () => window.removeEventListener(\"resize\", onResize);\n  }, [open, place]);\n\n  /* -- dismissal --------------------------------------------------------- */\n\n  useEffect(() => {\n    if (!open && !mobileOpen) return;\n    const onPointer = (event: globalThis.PointerEvent) => {\n      if (navRef.current?.contains(event.target as Node)) return;\n      setOpen(\"\");\n      setMobileOpen(false);\n    };\n    document.addEventListener(\"pointerdown\", onPointer);\n    return () => document.removeEventListener(\"pointerdown\", onPointer);\n  }, [open, mobileOpen, setOpen]);\n\n  useEffect(() => {\n    if (!open || !focusPanelOnOpen.current || !geom) return;\n    focusPanelOnOpen.current = false;\n    focusables(viewportRef.current)[0]?.focus();\n  }, [open, geom]);\n\n  const close = (refocus: boolean) => {\n    const current = openRef.current;\n    clearTimer();\n    setOpen(\"\");\n    if (refocus && current) triggerRefs.current[current]?.focus();\n  };\n\n  /* -- pointer intent ---------------------------------------------------- */\n\n  const onTriggerEnter = (event: PointerEvent, id: string) => {\n    if (event.pointerType !== \"mouse\") return;\n    setHovered(id);\n    if (openRef.current) {\n      clearTimer();\n      openedBy.current = \"hover\";\n      setOpen(id); // already open: switch instantly, no intent delay\n    } else {\n      schedule(() => {\n        openedBy.current = \"hover\";\n        setOpen(id);\n      }, openDelay);\n    }\n  };\n\n  const onLinkEnter = (event: PointerEvent, id: string) => {\n    if (event.pointerType !== \"mouse\") return;\n    setHovered(id);\n    if (openRef.current) schedule(() => setOpen(\"\"), closeDelay);\n    else clearTimer();\n  };\n\n  const onRegionLeave = (event: PointerEvent) => {\n    if (event.pointerType !== \"mouse\") return;\n    if (openRef.current) schedule(() => setOpen(\"\"), closeDelay);\n    else clearTimer();\n  };\n\n  const onRegionEnter = (event: PointerEvent) => {\n    if (event.pointerType !== \"mouse\") return;\n    if (openRef.current) clearTimer();\n  };\n\n  const onTriggerClick = (id: string) => {\n    clearTimer();\n    if (openRef.current === id) {\n      // A hover-opened panel stays put on the click that usually follows.\n      if (openedBy.current === \"hover\") openedBy.current = \"click\";\n      else setOpen(\"\");\n      return;\n    }\n    openedBy.current = \"click\";\n    setOpen(id);\n  };\n\n  /* -- keyboard ---------------------------------------------------------- */\n\n  // Top-level controls are each <li>'s own <a>/<button> (links may come from a\n  // render prop, so they are found structurally instead of by attribute).\n  const topItems = () =>\n    Array.from(\n      listRef.current?.querySelectorAll<HTMLElement>(\":scope > li > a, :scope > li > button\") ?? [],\n    );\n  const idOf = (el: Element | null | undefined) =>\n    el?.closest<HTMLElement>(\"li[data-nav-id]\")?.dataset.navId;\n\n  const onListKeyDown = (event: KeyboardEvent<HTMLUListElement>) => {\n    const tops = topItems();\n    const target = event.target as HTMLElement;\n    const at = tops.indexOf(target);\n    if (at < 0) return;\n    const item = items.find((i) => i.id === idOf(target));\n    if (!item) return;\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      const delta = event.key === \"ArrowRight\" ? 1 : -1;\n      const next = tops[(at + delta + tops.length) % tops.length];\n      next?.focus();\n      // Keep an open menu following focus across panel triggers.\n      const nextId = idOf(next);\n      if (openRef.current && nextId) setOpen(panelIds.includes(nextId) ? nextId : \"\");\n    } else if (event.key === \"Home\" || event.key === \"End\") {\n      event.preventDefault();\n      tops[event.key === \"Home\" ? 0 : tops.length - 1]?.focus();\n    } else if (hasPanel(item) && event.key === \"ArrowDown\") {\n      event.preventDefault();\n      if (openRef.current === item.id) focusables(viewportRef.current)[0]?.focus();\n      else {\n        focusPanelOnOpen.current = true;\n        openedBy.current = \"click\";\n        setOpen(item.id);\n      }\n    } else if (hasPanel(item) && event.key === \"Tab\" && !event.shiftKey && openRef.current === item.id) {\n      // The panel lives after the bar in the DOM; walk Tab into it.\n      const first = focusables(viewportRef.current)[0];\n      if (first) {\n        event.preventDefault();\n        first.focus();\n      }\n    } else if (event.key === \"Escape\" && openRef.current) {\n      event.preventDefault();\n      close(true);\n    }\n  };\n\n  const onViewportKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    const links = focusables(viewportRef.current);\n    const at = links.indexOf(document.activeElement as HTMLElement);\n    const trigger = triggerRefs.current[openRef.current];\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      close(true);\n    } else if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      const delta = event.key === \"ArrowDown\" ? 1 : -1;\n      links[(at + delta + links.length) % links.length]?.focus();\n    } else if (event.key === \"Tab\" && event.shiftKey && at === 0 && trigger) {\n      event.preventDefault();\n      trigger.focus();\n    } else if (event.key === \"Tab\" && !event.shiftKey && at === links.length - 1 && trigger) {\n      // Leaving the panel forward: continue to the item after its trigger.\n      const tops = topItems();\n      const after = tops[tops.indexOf(trigger) + 1] ?? focusables(actionsRef.current)[0];\n      close(false);\n      if (after) {\n        event.preventDefault();\n        after.focus();\n      }\n    }\n  };\n\n  /* -- render ------------------------------------------------------------ */\n\n  const openItem = items.find((i) => i.id === open);\n  const indicator = hovered ?? (open || null);\n  const morph = reduce || snap.current ? { duration: 0 } : SPRING_LAYOUT;\n\n  return (\n    <nav\n      ref={navRef}\n      aria-label={ariaLabel}\n      className={cn(\"relative z-40 w-full\", className)}\n      onBlur={(event) => {\n        const next = event.relatedTarget as Node | null;\n        if (next && !navRef.current?.contains(next)) {\n          setOpen(\"\");\n          setMobileOpen(false);\n        }\n      }}\n    >\n      <div className=\"flex h-14 items-center gap-3 rounded-2xl border border-border bg-card/90 px-3 shadow-sm backdrop-blur-md\">\n        {logo ? <div className=\"flex shrink-0 items-center pl-1 pr-2\">{logo}</div> : null}\n\n        {/* layoutRoot: the hover pill's shared layout stays local to the bar. */}\n        <motion.ul\n          ref={listRef}\n          layoutRoot\n          className=\"relative hidden items-center gap-0.5 md:flex\"\n          onKeyDown={onListKeyDown}\n          onPointerEnter={onRegionEnter}\n          onPointerLeave={(e) => {\n            setHovered(null);\n            onRegionLeave(e);\n          }}\n        >\n          {items.map((item) => {\n            const isOpen = open === item.id;\n            const pill =\n              indicator === item.id ? (\n                <motion.span\n                  layoutId={`${uid}-pill`}\n                  aria-hidden\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                  className=\"absolute inset-0 -z-10 rounded-lg bg-muted\"\n                />\n              ) : null;\n            const topClass = cn(\n              \"relative isolate inline-flex h-9 items-center gap-1 rounded-lg px-3 text-sm font-medium outline-none transition-colors\",\n              \"focus-visible:ring-2 focus-visible:ring-ring\",\n              isOpen || indicator === item.id ? \"text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n            );\n            if (!hasPanel(item)) {\n              return (\n                <li\n                  key={item.id}\n                  data-nav-id={item.id}\n                  className=\"relative\"\n                  onPointerEnter={(e) => onLinkEnter(e, item.id)}\n                >\n                  {pill}\n                  {(item.render ?? defaultLink)({\n                    href: item.href,\n                    className: topClass,\n                    children: item.label,\n                  })}\n                </li>\n              );\n            }\n            return (\n              <li\n                key={item.id}\n                data-nav-id={item.id}\n                className=\"relative\"\n                onPointerEnter={(e) => onTriggerEnter(e, item.id)}\n              >\n                {pill}\n                <button\n                  ref={(el) => {\n                    triggerRefs.current[item.id] = el;\n                  }}\n                  type=\"button\"\n                  aria-expanded={isOpen}\n                  aria-controls={isOpen ? panelId(item.id) : undefined}\n                  onClick={() => onTriggerClick(item.id)}\n                  className={topClass}\n                >\n                  {item.label}\n                  <motion.span\n                    aria-hidden\n                    animate={{ rotate: isOpen ? 180 : 0 }}\n                    transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                    className=\"inline-flex\"\n                  >\n                    <ChevronDown className=\"size-3.5 opacity-70\" />\n                  </motion.span>\n                </button>\n              </li>\n            );\n          })}\n        </motion.ul>\n\n        <div ref={actionsRef} className=\"ml-auto hidden items-center gap-2 md:flex\">\n          {actions}\n        </div>\n\n        <button\n          type=\"button\"\n          aria-label={mobileOpen ? \"Close menu\" : \"Open menu\"}\n          aria-expanded={mobileOpen}\n          onClick={() => setMobileOpen((o) => !o)}\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\") setMobileOpen(false);\n          }}\n          className=\"relative ml-auto inline-flex size-9 items-center justify-center rounded-lg text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring md:hidden\"\n        >\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={mobileOpen ? \"close\" : \"open\"}\n              aria-hidden\n              initial={reduce ? { opacity: 0 } : { opacity: 0, rotate: -90, scale: 0.6 }}\n              animate={{ opacity: 1, rotate: 0, scale: 1 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, rotate: 90, scale: 0.6 }}\n              transition={SPRING_SWAP}\n              className=\"inline-flex\"\n            >\n              {mobileOpen ? <X className=\"size-5\" /> : <Menu className=\"size-5\" />}\n            </motion.span>\n          </AnimatePresence>\n        </button>\n      </div>\n\n      {/* Desktop viewport ------------------------------------------------ */}\n      <AnimatePresence>\n        {openItem && hasPanel(openItem) ? (\n          // biome-ignore lint/a11y/noStaticElementInteractions: hover-intent region and key delegation for the panel's links\n          <div\n            key=\"viewport\"\n            className=\"pointer-events-none absolute left-0 top-full hidden w-full pt-2 md:block\"\n            onKeyDown={onViewportKeyDown}\n          >\n            <motion.div\n              ref={viewportRef}\n              onPointerEnter={onRegionEnter}\n              onPointerLeave={onRegionLeave}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.98 }}\n              animate={{\n                opacity: 1,\n                y: 0,\n                scale: 1,\n                // Sizing is this component's point: the viewport morphs its\n                // width/height between measured panels (and glides under the\n                // active trigger), so these layout values animate on purpose.\n                x: geom?.x ?? 0,\n                width: geom?.w ?? \"auto\",\n                height: geom?.h ?? \"auto\",\n              }}\n              exit={\n                reduce\n                  ? { opacity: 0, transition: { duration: 0.12 } }\n                  : { opacity: 0, y: -4, scale: 0.98, transition: { duration: 0.14 } }\n              }\n              transition={{ ...SPRING_PANEL, x: morph, width: morph, height: morph }}\n              style={{ transformOrigin: \"top left\" }}\n              className=\"pointer-events-auto relative overflow-hidden rounded-2xl border border-border bg-popover text-popover-foreground shadow-xl\"\n            >\n              <AnimatePresence initial={false} custom={{ dir, reduce }}>\n                <motion.div\n                  key={openItem.id}\n                  id={panelId(openItem.id)}\n                  custom={{ dir, reduce }}\n                  variants={panelVariants}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                  transition={reduce ? { duration: 0.12 } : SPRING_LAYOUT}\n                  className=\"absolute left-0 top-0\"\n                >\n                  <Measured onSize={sizeCallback(openItem.id)}>\n                    <PanelBody item={openItem} onNavigate={() => close(false)} />\n                  </Measured>\n                </motion.div>\n              </AnimatePresence>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n\n      {/* Mobile sheet ----------------------------------------------------- */}\n      <AnimatePresence>\n        {mobileOpen ? (\n          <MobileSheet\n            items={items}\n            actions={actions}\n            reduce={reduce}\n            onClose={(refocus) => {\n              setMobileOpen(false);\n              if (refocus) navRef.current?.querySelector<HTMLElement>(\"[aria-label='Close menu']\")?.focus();\n            }}\n          />\n        ) : null}\n      </AnimatePresence>\n    </nav>\n  );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Mobile                                                                    */\n/* ------------------------------------------------------------------------ */\n\nfunction MobileSheet({\n  items,\n  actions,\n  reduce,\n  onClose,\n}: {\n  items: NavigationMenuItem[];\n  actions?: ReactNode;\n  reduce: boolean;\n  onClose: (refocus: boolean) => void;\n}) {\n  const [section, setSection] = useState<string | null>(null);\n  const uid = useId();\n\n  return (\n    <motion.div\n      initial={reduce ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.98 }}\n      animate={{ opacity: 1, y: 0, scale: 1 }}\n      exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.98, transition: { duration: 0.14 } }}\n      transition={SPRING_PANEL}\n      style={{ transformOrigin: \"top center\" }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") {\n          e.preventDefault();\n          onClose(true);\n        }\n      }}\n      className=\"absolute inset-x-0 top-full mt-2 max-h-[70vh] overflow-y-auto rounded-2xl border border-border bg-popover p-2 text-popover-foreground shadow-xl [scrollbar-width:none] md:hidden [&::-webkit-scrollbar]:hidden\"\n    >\n      <ul className=\"flex flex-col\">\n        {items.map((item) => {\n          if (!hasPanel(item)) {\n            return (\n              <li key={item.id}>\n                {(item.render ?? defaultLink)({\n                  href: item.href,\n                  onClick: () => onClose(false),\n                  className:\n                    \"flex h-12 items-center rounded-xl px-3 text-sm font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:bg-muted\",\n                  children: item.label,\n                })}\n              </li>\n            );\n          }\n          const expanded = section === item.id;\n          const regionId = `${uid}-${item.id}`;\n          return (\n            <li key={item.id} className=\"border-b border-border last:border-b-0\">\n              <button\n                type=\"button\"\n                aria-expanded={expanded}\n                aria-controls={regionId}\n                onClick={() => setSection(expanded ? null : item.id)}\n                className=\"flex h-12 w-full items-center justify-between rounded-xl px-3 text-sm font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:bg-muted\"\n              >\n                {item.label}\n                <motion.span\n                  aria-hidden\n                  animate={{ rotate: expanded ? 180 : 0 }}\n                  transition={reduce ? { duration: 0 } : SPRING_SWAP}\n                  className=\"inline-flex text-muted-foreground\"\n                >\n                  <ChevronDown className=\"size-4\" />\n                </motion.span>\n              </button>\n              <AnimatePresence initial={false}>\n                {expanded ? (\n                  <motion.div\n                    id={regionId}\n                    // Accordion height is the layout this section exists for,\n                    // so height animates here on purpose.\n                    initial={{ height: 0, opacity: 0 }}\n                    animate={{ height: \"auto\", opacity: 1 }}\n                    exit={{ height: 0, opacity: 0, transition: { duration: 0.16 } }}\n                    transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                    className=\"overflow-hidden\"\n                  >\n                    <div className=\"flex flex-col gap-1 pb-2\">\n                      {item.sections.map((s) => (\n                        <div key={sectionKey(s)}>\n                          {s.title ? (\n                            <p className=\"px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">\n                              {s.title}\n                            </p>\n                          ) : null}\n                          <ul>\n                            {s.links.map((link) => (\n                              <li key={link.href}>\n                                <LinkRow link={link} onNavigate={() => onClose(false)} />\n                              </li>\n                            ))}\n                          </ul>\n                        </div>\n                      ))}\n                    </div>\n                  </motion.div>\n                ) : null}\n              </AnimatePresence>\n            </li>\n          );\n        })}\n      </ul>\n      {actions ? (\n        <div className=\"mt-2 flex flex-wrap items-center gap-2 border-t border-border px-1 pt-3\">\n          {actions}\n        </div>\n      ) : null}\n    </motion.div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}