{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"morphing-nav","type":"registry:component","title":"Morphing nav","description":"One dropdown panel reshapes and slides as you move between menu items.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":["motion"],"registryDependencies":["utils"],"files":[{"path":"components/morphing-nav.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport type NavLink = { title: string; description: string; icon: React.ReactNode };\nexport type NavSection = { label: string; columns?: 1 | 2; links: NavLink[] };\n\n// Long enough that sweeping the cursor across the bar on the way somewhere\n// else doesn't flash a panel open, short enough to feel immediate.\nconst OPEN_INTENT = 80;\n// Grace before closing, so a cursor that clips the edge on its way into the\n// panel doesn't lose it.\nconst CLOSE_GRACE = 180;\n// How far the old and new content slide during a morph, in px.\nconst SLIDE = 48;\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n// One spring drives size, position and caret, so they arrive together.\n// No bounce: an overshooting panel would uncover empty space.\nconst MORPH = { type: \"spring\", duration: 0.3, bounce: 0 } as const;\nconst INSTANT = { duration: 0 } as const;\nconst CARET = 16;\n\ntype Size = { w: number; h: number };\ntype Active = { index: number; center: number };\n\nexport function MorphingNav({\n  sections,\n  label = \"Main\",\n  brand,\n  action,\n  className,\n}: {\n  sections: NavSection[];\n  label?: string;\n  brand?: React.ReactNode;\n  action?: React.ReactNode;\n  className?: string;\n}) {\n  const reduce = !!useReducedMotion();\n  const navRef = useRef<HTMLElement>(null);\n  const triggers = useRef<(HTMLButtonElement | null)[]>([]);\n  const measures = useRef<(HTMLDivElement | null)[]>([]);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const afterRef = useRef<HTMLDivElement>(null);\n  const [active, setActive] = useState<Active | null>(null);\n  const [direction, setDirection] = useState(1);\n  const [sizes, setSizes] = useState<Size[]>([]);\n  const [navWidth, setNavWidth] = useState(0);\n  // Each open gets a fresh panel, so reopening never morphs from a stale spot.\n  const [session, setSession] = useState(0);\n  const openTimer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const closeTimer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  const focusFirst = useRef(false);\n  const latest = useRef(active);\n  const panelId = useId();\n\n  useEffect(() => {\n    latest.current = active;\n  });\n\n  const centerOf = (index: number) => {\n    const nav = navRef.current?.getBoundingClientRect();\n    const box = triggers.current[index]?.getBoundingClientRect();\n    return nav && box ? box.left - nav.left + box.width / 2 : 0;\n  };\n\n  // Sizes come from hidden copies of every section, so the panel knows where\n  // it's going before the new content has even rendered.\n  useEffect(() => {\n    const nav = navRef.current;\n    if (!nav) return;\n    const observer = new ResizeObserver(() => {\n      setNavWidth(nav.offsetWidth);\n      setSizes(\n        measures.current.map((m) => ({ w: m?.offsetWidth ?? 0, h: m?.offsetHeight ?? 0 })),\n      );\n      setActive((a) => a && { ...a, center: centerOf(a.index) });\n    });\n    observer.observe(nav);\n    measures.current.forEach((m) => m && observer.observe(m));\n    return () => observer.disconnect();\n  }, [sections]);\n\n  useEffect(\n    () => () => {\n      clearTimeout(openTimer.current);\n      clearTimeout(closeTimer.current);\n    },\n    [],\n  );\n\n  // Taps outside close it on touch, where there's no pointer to leave.\n  useEffect(() => {\n    if (!active) return;\n    const onDown = (e: PointerEvent) => {\n      if (!navRef.current?.contains(e.target as Node)) setActive(null);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [active]);\n\n  // Only the incoming section: during a morph the outgoing copy is still in\n  // the DOM, and keyboard focus must never land on links that are leaving.\n  const liveLinks = () => {\n    const index = latest.current?.index;\n    return [\n      ...(panelRef.current?.querySelectorAll<HTMLElement>(`[data-section=\"${index}\"] a`) ?? []),\n    ];\n  };\n\n  useEffect(() => {\n    if (!active || !focusFirst.current) return;\n    focusFirst.current = false;\n    liveLinks()[0]?.focus();\n  }, [active]);\n\n  const open = (index: number) => {\n    clearTimeout(openTimer.current);\n    clearTimeout(closeTimer.current);\n    const current = latest.current;\n    if (current?.index === index) return;\n    if (current) setDirection(index > current.index ? 1 : -1);\n    else setSession((s) => s + 1);\n    const next = { index, center: centerOf(index) };\n    latest.current = next;\n    setActive(next);\n  };\n\n  const close = (refocus = false) => {\n    clearTimeout(openTimer.current);\n    clearTimeout(closeTimer.current);\n    const current = latest.current;\n    latest.current = null;\n    setActive(null);\n    if (refocus && current) triggers.current[current.index]?.focus();\n  };\n\n  const focusTrigger = (index: number) => {\n    const count = sections.length;\n    triggers.current[(index + count) % count]?.focus();\n  };\n\n  const onTriggerKeyDown = (index: number, e: React.KeyboardEvent) => {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      focusFirst.current = true;\n      if (active?.index === index) {\n        focusFirst.current = false;\n        liveLinks()[0]?.focus();\n      } else open(index);\n    } else if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n      e.preventDefault();\n      focusTrigger(index + (e.key === \"ArrowRight\" ? 1 : -1));\n    } else if (e.key === \"Escape\" && active) {\n      e.preventDefault();\n      close();\n    } else if (e.key === \"Tab\" && !e.shiftKey && active?.index === index) {\n      // The panel sits after the whole bar in the DOM; Tab goes into it\n      // from its own trigger, as if it followed the trigger directly.\n      const first = liveLinks()[0];\n      if (first) {\n        e.preventDefault();\n        first.focus();\n      }\n    }\n  };\n\n  const onPanelKeyDown = (e: React.KeyboardEvent) => {\n    if (!active) return;\n    const links = liveLinks();\n    const at = links.indexOf(document.activeElement as HTMLElement);\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      close(true);\n    } else if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      const step = e.key === \"ArrowDown\" ? 1 : -1;\n      links[(at + step + links.length) % links.length]?.focus();\n    } else if (e.key === \"Tab\" && e.shiftKey && at === 0) {\n      e.preventDefault();\n      triggers.current[active.index]?.focus();\n    } else if (e.key === \"Tab\" && !e.shiftKey && at === links.length - 1) {\n      const next = triggers.current[active.index + 1];\n      const after = afterRef.current?.querySelector<HTMLElement>(\"a, button\");\n      if (next) {\n        e.preventDefault();\n        next.focus();\n      } else if (after) {\n        e.preventDefault();\n        close();\n        after.focus();\n      } else close();\n    }\n  };\n\n  const size = active ? sizes[active.index] : undefined;\n  const width = size ? Math.min(size.w, navWidth) : 0;\n  const x = active ? Math.max(0, Math.min(active.center - width / 2, navWidth - width)) : 0;\n  // Keeps the caret off the rounded corners.\n  const caretX = active\n    ? Math.max(12, Math.min(active.center - x - CARET / 2, width - CARET - 12))\n    : 0;\n  const morph = reduce ? INSTANT : MORPH;\n\n  return (\n    <nav\n      ref={navRef}\n      aria-label={label}\n      className={cn(\"relative w-[min(600px,100%)]\", className)}\n      onPointerEnter={(e) => {\n        if (e.pointerType !== \"touch\") clearTimeout(closeTimer.current);\n      }}\n      onPointerLeave={(e) => {\n        if (e.pointerType === \"touch\") return;\n        clearTimeout(openTimer.current);\n        if (latest.current) {\n          clearTimeout(closeTimer.current);\n          closeTimer.current = setTimeout(() => close(), CLOSE_GRACE);\n        }\n      }}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) close();\n      }}\n    >\n      {/* 16 = 10 (trigger radius) + 6 (padding): concentric corners. */}\n      <div className=\"flex h-[52px] items-center gap-1 rounded-2xl bg-background p-1.5 shadow-raised\">\n        {brand && <div className=\"flex shrink-0 items-center px-2.5 max-sm:hidden\">{brand}</div>}\n        <ul className=\"flex min-w-0 flex-1 items-center gap-0.5\">\n          {sections.map((section, index) => {\n            const on = active?.index === index;\n            return (\n              <li key={section.label}>\n                <button\n                  ref={(el) => {\n                    triggers.current[index] = el;\n                  }}\n                  type=\"button\"\n                  aria-expanded={on}\n                  aria-controls={on ? panelId : undefined}\n                  onPointerEnter={(e) => {\n                    if (e.pointerType === \"touch\") return;\n                    clearTimeout(openTimer.current);\n                    // Already open: follow the cursor at once. Closed: wait\n                    // a beat to be sure the cursor means it.\n                    if (latest.current) open(index);\n                    else openTimer.current = setTimeout(() => open(index), OPEN_INTENT);\n                  }}\n                  onPointerLeave={() => {\n                    if (!latest.current) clearTimeout(openTimer.current);\n                  }}\n                  onClick={(e) => {\n                    // detail is 0 for Enter and Space; those toggle, like a tap.\n                    const keyboardOrTouch =\n                      e.detail === 0 ||\n                      (e.nativeEvent as PointerEvent).pointerType === \"touch\";\n                    if (on && keyboardOrTouch) close();\n                    else open(index);\n                  }}\n                  onFocus={() => {\n                    // With the panel open, focus moving along the bar moves it too.\n                    if (latest.current && latest.current.index !== index) open(index);\n                  }}\n                  onKeyDown={(e) => onTriggerKeyDown(index, e)}\n                  className={cn(\n                    \"flex h-10 touch-manipulation items-center gap-1 rounded-[10px] px-2.5 text-sm whitespace-nowrap outline-hidden transition-[scale,color,background-color] duration-150 ease-out select-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-foreground active:scale-[0.96] motion-reduce:transition-[color,background-color] sm:px-3\",\n                    on\n                      ? \"bg-muted text-foreground\"\n                      : \"text-muted-foreground hover:text-foreground\",\n                  )}\n                >\n                  {section.label}\n                  <svg\n                    viewBox=\"0 0 12 12\"\n                    className={cn(\n                      \"size-3 transition-[rotate] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] max-sm:hidden motion-reduce:transition-none\",\n                      on && \"rotate-180\",\n                    )}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={1.5}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    aria-hidden\n                  >\n                    <path d=\"m3 4.75 3 3 3-3\" />\n                  </svg>\n                </button>\n              </li>\n            );\n          })}\n        </ul>\n        {action && (\n          <div ref={afterRef} className=\"shrink-0 max-sm:hidden\">\n            {action}\n          </div>\n        )}\n      </div>\n\n      <AnimatePresence>\n        {active && size && (\n          <motion.div\n            key={session}\n            className=\"absolute top-full left-0 mt-3\"\n            initial={{ opacity: 0, scale: 0.97, x, width, height: size.h }}\n            animate={{ opacity: 1, scale: 1, x, width, height: size.h }}\n            // Leaves faster and smaller than it came: the exit shouldn't hold the eye.\n            exit={{ opacity: 0, scale: 0.98, transition: { duration: 0.15, ease: EASE_OUT } }}\n            transition={{\n              default: morph,\n              opacity: { duration: 0.2, ease: EASE_OUT },\n              scale: { duration: reduce ? 0 : 0.2, ease: EASE_OUT },\n            }}\n            // Grows out of the caret, which points at the trigger.\n            style={{ transformOrigin: `${caretX + CARET / 2}px 0px` }}\n          >\n            {/* Invisible bridge over the gap, so the cursor never \"leaves\"\n                on its way down from the bar. */}\n            <div aria-hidden className=\"absolute inset-x-0 -top-3 h-3\" />\n            <motion.svg\n              aria-hidden\n              viewBox=\"0 0 16 9\"\n              width={CARET}\n              height={9}\n              // Overlaps the panel by 1px to hide its ring under the caret.\n              className=\"absolute -top-2 left-0 z-10 fill-background stroke-foreground/[0.08]\"\n              initial={{ x: caretX }}\n              animate={{ x: caretX }}\n              transition={morph}\n            >\n              <path d=\"M0 9 8 1l8 8\" strokeWidth={1} />\n            </motion.svg>\n            <div\n              ref={panelRef}\n              id={panelId}\n              onKeyDown={onPanelKeyDown}\n              // 16 = 8 (link radius) + 8 (padding): concentric corners.\n              className=\"relative size-full overflow-hidden rounded-2xl bg-background shadow-raised\"\n            >\n              <AnimatePresence initial={false} custom={direction}>\n                <motion.div\n                  key={active.index}\n                  data-section={active.index}\n                  custom={direction}\n                  variants={reduce ? FADE : SLIDES}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                  className=\"absolute top-0 left-0\"\n                  // Pinned to its final width so text never reflows mid-morph.\n                  style={{ width }}\n                >\n                  <SectionLinks\n                    section={sections[active.index]}\n                    onNavigate={() => close()}\n                  />\n                </motion.div>\n              </AnimatePresence>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      {/* Hidden copies, measured for their natural size. */}\n      <div aria-hidden inert className=\"pointer-events-none invisible absolute top-0 left-0 w-full\">\n        {sections.map((section, index) => (\n          <div\n            key={section.label}\n            ref={(el) => {\n              measures.current[index] = el;\n            }}\n            className=\"absolute top-0 left-0 w-max max-w-full\"\n          >\n            <SectionLinks section={section} />\n          </div>\n        ))}\n      </div>\n    </nav>\n  );\n}\n\nconst SLIDES = {\n  enter: (dir: number) => ({ x: dir * SLIDE, opacity: 0, filter: \"blur(4px)\" }),\n  center: {\n    x: 0,\n    opacity: 1,\n    filter: \"blur(0px)\",\n    transition: { duration: 0.25, ease: EASE_OUT },\n  },\n  exit: (dir: number) => ({\n    x: -dir * SLIDE,\n    opacity: 0,\n    filter: \"blur(4px)\",\n    transition: { duration: 0.18, ease: EASE_OUT },\n  }),\n};\n\nconst FADE = {\n  enter: { opacity: 0 },\n  center: { opacity: 1, transition: { duration: 0.2, ease: EASE_OUT } },\n  exit: { opacity: 0, transition: { duration: 0.15, ease: EASE_OUT } },\n};\n\nfunction SectionLinks({\n  section,\n  onNavigate,\n}: {\n  section: NavSection;\n  onNavigate?: () => void;\n}) {\n  return (\n    <ul className={cn(\"grid gap-0.5 p-2\", section.columns === 2 && \"sm:grid-cols-2\")}>\n      {section.links.map((link) => (\n        <li key={link.title} className=\"min-w-0\">\n          <a\n            href=\"#\"\n            onClick={(e) => {\n              e.preventDefault();\n              onNavigate?.();\n            }}\n            className=\"flex items-start gap-3 rounded-lg p-3 pr-4 outline-hidden transition-[background-color] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-foreground hover:bg-muted\"\n          >\n            <svg\n              viewBox=\"0 0 24 24\"\n              // Nudged down to sit on the title's first line, not above it.\n              className=\"mt-px size-[18px] shrink-0 text-muted-foreground\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={1.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              aria-hidden\n            >\n              {link.icon}\n            </svg>\n            <span className=\"min-w-0\">\n              <span className=\"block text-sm font-medium text-foreground\">{link.title}</span>\n              <span className=\"block truncate text-sm text-muted-foreground\">{link.description}</span>\n            </span>\n          </a>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nconst SECTIONS: NavSection[] = [\n  {\n    label: \"Products\",\n    columns: 2,\n    links: [\n      {\n        title: \"Analytics\",\n        description: \"Live dashboards for every metric\",\n        icon: <path d=\"M4.5 19.5v-6M9.5 19.5v-10M14.5 19.5v-7M19.5 19.5V5.5\" />,\n      },\n      {\n        title: \"Automations\",\n        description: \"Run workflows on any event\",\n        icon: <path d=\"M13 3.5 5.5 13.5H12l-1 7 7.5-10H12Z\" />,\n      },\n      {\n        title: \"Storage\",\n        description: \"Files and backups in one place\",\n        icon: (\n          <>\n            <ellipse cx=\"12\" cy=\"6.5\" rx=\"7\" ry=\"3\" />\n            <path d=\"M5 6.5v11c0 1.66 3.13 3 7 3s7-1.34 7-3v-11M5 12c0 1.66 3.13 3 7 3s7-1.34 7-3\" />\n          </>\n        ),\n      },\n      {\n        title: \"Security\",\n        description: \"Access control and audit logs\",\n        icon: <path d=\"M12 3.5 5 6v5.5c0 4.2 3 7.6 7 9 4-1.4 7-4.8 7-9V6Z\" />,\n      },\n    ],\n  },\n  {\n    label: \"Solutions\",\n    links: [\n      {\n        title: \"Startups\",\n        description: \"Launch fast with sensible defaults\",\n        icon: <path d=\"M12 20.5v-4M8 16.5l4-13 4 13ZM9.5 12.5h5\" />,\n      },\n      {\n        title: \"Enterprise\",\n        description: \"Scale with SSO and dedicated support\",\n        icon: (\n          <>\n            <path d=\"M4.5 20.5v-15h9v15M13.5 9.5h6v11M3 20.5h18\" />\n            <path d=\"M7.5 9h3M7.5 12.5h3M7.5 16h3M16.5 13h.01M16.5 16.5h.01\" />\n          </>\n        ),\n      },\n      {\n        title: \"Agencies\",\n        description: \"Manage every client from one account\",\n        icon: (\n          <>\n            <circle cx=\"9\" cy=\"8.5\" r=\"3\" />\n            <path d=\"M3.5 19.5a5.5 5.5 0 0 1 11 0M15.5 5.5a3 3 0 0 1 0 6M17.5 14.5a5.5 5.5 0 0 1 3 5\" />\n          </>\n        ),\n      },\n    ],\n  },\n  {\n    label: \"Resources\",\n    columns: 2,\n    links: [\n      {\n        title: \"Docs\",\n        description: \"Guides and API reference\",\n        icon: (\n          <>\n            <path d=\"M5.5 4.5h9l4 4v11h-13Z\" />\n            <path d=\"M14.5 4.5v4h4M8.5 12.5h7M8.5 16h5\" />\n          </>\n        ),\n      },\n      {\n        title: \"Changelog\",\n        description: \"What shipped this week\",\n        icon: (\n          <>\n            <circle cx=\"12\" cy=\"12\" r=\"8.5\" />\n            <path d=\"M12 7.5V12l3 2\" />\n          </>\n        ),\n      },\n      {\n        title: \"Community\",\n        description: \"Ask questions, share builds\",\n        icon: <path d=\"M4.5 18.5v-11a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-9Z\" />,\n      },\n      {\n        title: \"Templates\",\n        description: \"Starter projects to fork\",\n        icon: (\n          <>\n            <rect x=\"3.5\" y=\"3.5\" width=\"7\" height=\"7\" rx=\"1.5\" />\n            <rect x=\"13.5\" y=\"3.5\" width=\"7\" height=\"7\" rx=\"1.5\" />\n            <rect x=\"3.5\" y=\"13.5\" width=\"7\" height=\"7\" rx=\"1.5\" />\n            <rect x=\"13.5\" y=\"13.5\" width=\"7\" height=\"7\" rx=\"1.5\" />\n          </>\n        ),\n      },\n    ],\n  },\n  {\n    label: \"Pricing\",\n    links: [\n      {\n        title: \"Plans\",\n        description: \"Compare every tier side by side\",\n        icon: (\n          <>\n            <rect x=\"3.5\" y=\"5.5\" width=\"17\" height=\"13\" rx=\"2\" />\n            <path d=\"M3.5 9.5h17M7 14.5h3\" />\n          </>\n        ),\n      },\n      {\n        title: \"Calculator\",\n        description: \"Estimate your monthly bill\",\n        icon: (\n          <>\n            <rect x=\"5.5\" y=\"3.5\" width=\"13\" height=\"17\" rx=\"2\" />\n            <path d=\"M8.5 7.5h7M9 12h.01M12 12h.01M15 12h.01M9 16h.01M12 16h.01M15 16h.01\" />\n          </>\n        ),\n      },\n    ],\n  },\n];\n\nexport default function MorphingNavDemo() {\n  return (\n    // The panel floats over this reserved space, so opening, morphing and\n    // closing never change the demo's height.\n    <div className=\"h-[340px] w-[min(600px,100%)]\">\n      <MorphingNav\n        sections={SECTIONS}\n        brand={\n          <svg viewBox=\"0 0 20 20\" className=\"size-5 text-foreground\" aria-label=\"Home\" role=\"img\">\n            <circle cx=\"10\" cy=\"10\" r=\"7.25\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} />\n            <circle cx=\"10\" cy=\"10\" r=\"2.5\" fill=\"currentColor\" />\n          </svg>\n        }\n        action={\n          <a\n            href=\"#\"\n            onClick={(e) => e.preventDefault()}\n            className=\"flex h-10 items-center rounded-[10px] bg-foreground px-3.5 text-sm font-medium text-background outline-hidden transition-[scale] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-foreground active:scale-[0.96] motion-reduce:transition-none\"\n          >\n            Sign in\n          </a>\n        }\n      />\n    </div>\n  );\n}\n"},{"path":"hooks/use-reduced-motion.ts","type":"registry:hook","target":"hooks/use-reduced-motion.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// Motion's useReducedMotion reads the media query on the very first client\n// render, but the server can't know it, so every component that renders\n// differently under reduced motion broke hydration for those users. This\n// reports false during hydration, matching the server HTML, then the real\n// preference straight after, and follows it if it changes.\nconst query = \"(prefers-reduced-motion: reduce)\";\n\nfunction subscribe(onChange: () => void) {\n  const media = window.matchMedia(query);\n  media.addEventListener(\"change\", onChange);\n  return () => media.removeEventListener(\"change\", onChange);\n}\n\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(query).matches,\n    () => false,\n  );\n}\n"}],"cssVars":{"light":{"shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"shadow-raised":"var(--shadow-raised)"}},"categories":["navigation"],"docs":"From ui lab: https://lab.xevrion.dev/lab/morphing-nav"}