{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-nav",
  "type": "registry:component",
  "title": "Glass Nav",
  "description": "A floating liquid-glass pill navigation bar: backdrop-blur with a gradient-mask border (WWDC 2025 liquid glass look), logo, inline links, and an accent CTA. Collapses below `md` into a hamburger that opens a focus-trapped, `inert`-backed full overlay panel (Tab/Shift+Tab wrap, Escape closes, focus returns to the trigger).",
  "files": [
    {
      "path": "glass-nav.tsx",
      "target": "components/safari/glass-nav/glass-nav.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\n\nimport \"./glass-nav.css\";\n\nexport interface GlassNavLink {\n  label: string;\n  href: string;\n}\n\nexport interface GlassNavCta {\n  label: string;\n  href: string;\n}\n\nexport interface GlassNavProps {\n  /** Rendered top-left. Usually a wordmark or logo + text. */\n  brand: ReactNode;\n  links: GlassNavLink[];\n  cta?: GlassNavCta;\n  className?: string;\n}\n\nconst FOCUSABLE_SELECTOR =\n  'a[href], button:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Liquid-glass pill navigation: a floating backdrop-blur bar with a\n * gradient-mask border (top/bottom glow, transparent mid-line), logo,\n * inline links, and an accent CTA. Collapses to a hamburger below `md`\n * that opens a full-panel overlay menu.\n *\n * The mobile overlay is a real focus trap: focus moves to the panel on\n * open, Tab/Shift+Tab wrap inside it, Escape closes it, focus returns to\n * the trigger on close, and the rest of the page is marked `inert` so\n * assistive tech and stray Tab presses can't reach content behind it.\n */\nexport function GlassNav({ brand, links, cta, className }: GlassNavProps) {\n  const [open, setOpen] = useState(false);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const panelId = useId();\n\n  const close = useCallback(() => {\n    setOpen(false);\n    triggerRef.current?.focus();\n  }, []);\n\n  // Mark everything outside the nav `inert` while the mobile panel is\n  // open, so Tab / screen-reader virtual cursor can't leave the trap.\n  useEffect(() => {\n    if (!open) return undefined;\n\n    const siblings = Array.from(document.body.children).filter(\n      (el) => !el.contains(panelRef.current) && el !== panelRef.current,\n    ) as HTMLElement[];\n\n    const restored = siblings.map((el) => ({\n      el,\n      hadInert: el.hasAttribute(\"inert\"),\n    }));\n    for (const el of siblings) el.setAttribute(\"inert\", \"\");\n\n    const previousOverflow = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n\n    const panel = panelRef.current;\n    const firstFocusable = panel?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);\n    firstFocusable?.focus();\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        close();\n        return;\n      }\n\n      if (event.key !== \"Tab\" || !panel) return;\n\n      const focusable = Array.from(\n        panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),\n      );\n      if (focusable.length === 0) return;\n\n      const first = focusable[0]!;\n      const last = focusable[focusable.length - 1]!;\n\n      if (event.shiftKey && document.activeElement === first) {\n        event.preventDefault();\n        last.focus();\n      } else if (!event.shiftKey && document.activeElement === last) {\n        event.preventDefault();\n        first.focus();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown);\n      document.body.style.overflow = previousOverflow;\n      for (const { el, hadInert } of restored) {\n        if (!hadInert) el.removeAttribute(\"inert\");\n      }\n    };\n  }, [open, close]);\n\n  return (\n    <header className={`sm-nav-root ${className ?? \"\"}`}>\n      <nav\n        aria-label=\"Primary\"\n        className=\"sm-nav-bar sm-liquid-glass flex items-center justify-between gap-4 rounded-full px-4 py-2 md:px-5\"\n      >\n        <div className=\"sm-nav-brand shrink-0 text-sm font-semibold tracking-tight text-sm-text\">\n          {brand}\n        </div>\n\n        <ul className=\"hidden items-center gap-1 md:flex\">\n          {links.map((link) => (\n            <li key={link.href}>\n              <a\n                href={link.href}\n                className=\"sm-nav-link rounded-full px-3 py-1.5 text-sm text-sm-text-muted transition-colors hover:text-sm-text\"\n              >\n                {link.label}\n              </a>\n            </li>\n          ))}\n        </ul>\n\n        <div className=\"flex items-center gap-2\">\n          {cta ? (\n            <a\n              href={cta.href}\n              className=\"sm-nav-cta hidden rounded-full bg-sm-text px-4 py-1.5 text-sm font-medium text-sm-bg transition-transform md:inline-flex md:items-center hover:scale-[1.02] active:scale-[0.98]\"\n            >\n              {cta.label}\n            </a>\n          ) : null}\n\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            className=\"sm-nav-trigger flex h-9 w-9 items-center justify-center rounded-full md:hidden\"\n            aria-expanded={open}\n            aria-controls={panelId}\n            aria-label={open ? \"Close menu\" : \"Open menu\"}\n            onClick={() => setOpen((v) => !v)}\n          >\n            <span className=\"sm-nav-burger\" data-open={open} aria-hidden=\"true\">\n              <span />\n              <span />\n              <span />\n            </span>\n          </button>\n        </div>\n      </nav>\n\n      {open ? (\n        <div\n          id={panelId}\n          ref={panelRef}\n          role=\"dialog\"\n          aria-modal=\"true\"\n          aria-label=\"Menu\"\n          className=\"sm-nav-panel sm-liquid-glass fixed inset-x-4 top-20 z-50 rounded-2xl p-6 md:hidden\"\n        >\n          <ul className=\"flex flex-col gap-1\">\n            {links.map((link) => (\n              <li key={link.href}>\n                <a\n                  href={link.href}\n                  onClick={close}\n                  className=\"block rounded-xl px-3 py-3 text-2xl font-medium tracking-tight text-sm-text\"\n                >\n                  {link.label}\n                </a>\n              </li>\n            ))}\n          </ul>\n\n          {cta ? (\n            <a\n              href={cta.href}\n              onClick={close}\n              className=\"mt-4 block rounded-full bg-sm-text px-4 py-3 text-center text-base font-medium text-sm-bg\"\n            >\n              {cta.label}\n            </a>\n          ) : null}\n        </div>\n      ) : null}\n    </header>\n  );\n}\n"
    },
    {
      "path": "glass-nav.css",
      "target": "components/safari/glass-nav/glass-nav.css",
      "type": "registry:file",
      "content": "/*\n * Liquid-glass nav shell: floating pill bar + mobile overlay panel share\n * the same `.sm-liquid-glass` backdrop-blur + gradient-mask border recipe\n * (WWDC 2025 \"liquid glass\" look — glows top/bottom, transparent mid-line).\n */\n\n.sm-nav-root {\n  position: sticky;\n  top: 0;\n  z-index: 40;\n  padding: 12px 16px;\n}\n\n.sm-liquid-glass {\n  background: rgba(255, 255, 255, 0.06);\n  background-blend-mode: luminosity;\n  backdrop-filter: blur(16px);\n  -webkit-backdrop-filter: blur(16px);\n  box-shadow:\n    inset 0 1px 1px rgba(255, 255, 255, 0.12),\n    0 8px 32px rgba(0, 0, 0, 0.24);\n  position: relative;\n  overflow: hidden;\n}\n\n.sm-liquid-glass::before {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  padding: 1.4px;\n  background: linear-gradient(\n    180deg,\n    rgba(255, 255, 255, 0.45) 0%,\n    rgba(255, 255, 255, 0.15) 20%,\n    rgba(255, 255, 255, 0) 40%,\n    rgba(255, 255, 255, 0) 60%,\n    rgba(255, 255, 255, 0.15) 80%,\n    rgba(255, 255, 255, 0.45) 100%\n  );\n  -webkit-mask:\n    linear-gradient(#fff 0 0) content-box,\n    linear-gradient(#fff 0 0);\n  -webkit-mask-composite: xor;\n  mask-composite: exclude;\n  pointer-events: none;\n}\n\n:root.light .sm-liquid-glass {\n  background: rgba(0, 0, 0, 0.03);\n}\n\n:root.light .sm-liquid-glass::before {\n  background: linear-gradient(\n    180deg,\n    rgba(0, 0, 0, 0.16) 0%,\n    rgba(0, 0, 0, 0.05) 20%,\n    rgba(0, 0, 0, 0) 40%,\n    rgba(0, 0, 0, 0) 60%,\n    rgba(0, 0, 0, 0.05) 80%,\n    rgba(0, 0, 0, 0.16) 100%\n  );\n}\n\n.sm-nav-bar {\n  max-width: 960px;\n  margin: 0 auto;\n}\n\n.sm-nav-link {\n  position: relative;\n}\n\n.sm-nav-trigger {\n  background: transparent;\n  border: none;\n}\n\n:root.light .sm-nav-burger span {\n  --sm-nav-burger-color: #18181b;\n}\n\n/* Hamburger -> X morph. Bars are absolutely stacked and transform in\n   place so the icon never reflows. */\n.sm-nav-burger {\n  position: relative;\n  display: block;\n  width: 18px;\n  height: 12px;\n}\n\n.sm-nav-burger span {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  height: 1.5px;\n  border-radius: 1px;\n  background: var(--sm-nav-burger-color, #f4f4f5);\n  transition:\n    transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),\n    opacity 0.2s ease,\n    top 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.sm-nav-burger span:nth-child(1) {\n  top: 0;\n}\n\n.sm-nav-burger span:nth-child(2) {\n  top: 5.25px;\n}\n\n.sm-nav-burger span:nth-child(3) {\n  top: 10.5px;\n}\n\n.sm-nav-burger[data-open=\"true\"] span:nth-child(1) {\n  top: 5.25px;\n  transform: rotate(45deg);\n}\n\n.sm-nav-burger[data-open=\"true\"] span:nth-child(2) {\n  opacity: 0;\n}\n\n.sm-nav-burger[data-open=\"true\"] span:nth-child(3) {\n  top: 5.25px;\n  transform: rotate(-45deg);\n}\n\n.sm-nav-panel {\n  animation: sm-nav-panel-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;\n}\n\n@keyframes sm-nav-panel-in {\n  from {\n    opacity: 0;\n    transform: translateY(-12px) scale(0.98);\n  }\n  to {\n    opacity: 1;\n    transform: none;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .sm-nav-burger span {\n    transition: none;\n  }\n\n  .sm-nav-panel {\n    animation: none;\n  }\n}\n"
    },
    {
      "path": "demo.tsx",
      "target": "components/safari/glass-nav/demo.tsx",
      "type": "registry:file",
      "content": "\"use client\";\n\nimport { GlassNav } from \"./glass-nav\";\n\n/**\n * Isolated demo used by the future component catalog. Renders the nav\n * over a dark gradient surface so the glass blur has something to blur.\n */\nexport default function GlassNavDemo() {\n  return (\n    <div className=\"relative min-h-[420px] overflow-hidden rounded-2xl bg-[radial-gradient(circle_at_30%_20%,_#1a2244_0%,_#0a0a0b_60%)]\">\n      <GlassNav\n        brand=\"Safari Motion\"\n        links={[\n          { label: \"Components\", href: \"#components\" },\n          { label: \"Docs\", href: \"#docs\" },\n          { label: \"Pricing\", href: \"#pricing\" },\n        ]}\n        cta={{ label: \"Get started\", href: \"#get-started\" }}\n      />\n\n      <div className=\"flex flex-col gap-2 px-10 pt-20\">\n        <p className=\"max-w-md text-sm text-sm-text-muted\">\n          Scroll, resize below `md`, or tab through the nav to see the\n          liquid-glass bar collapse into a focus-trapped mobile panel.\n        </p>\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "meta": {
    "name": "glass-nav",
    "title": "Glass Nav",
    "description": "A floating liquid-glass pill navigation bar: backdrop-blur with a gradient-mask border (WWDC 2025 liquid glass look), logo, inline links, and an accent CTA. Collapses below `md` into a hamburger that opens a focus-trapped, `inert`-backed full overlay panel (Tab/Shift+Tab wrap, Escape closes, focus returns to the trigger).",
    "type": "component",
    "status": "prototype",
    "version": "0.1.0",
    "visibility": "internal",
    "tier": "free",
    "dependencies": {},
    "registryDependencies": [],
    "files": [
      {
        "path": "glass-nav.tsx",
        "target": "components/safari/glass-nav/glass-nav.tsx",
        "type": "registry:component"
      },
      {
        "path": "glass-nav.css",
        "target": "components/safari/glass-nav/glass-nav.css",
        "type": "registry:file"
      },
      {
        "path": "demo.tsx",
        "target": "components/safari/glass-nav/demo.tsx",
        "type": "registry:file"
      }
    ],
    "compatibility": {
      "react": ">=19.0.0",
      "tailwind": ">=4.0.0",
      "ssr": "client-only",
      "browsers": [
        "chrome >= 100",
        "safari >= 16",
        "firefox >= 100"
      ]
    },
    "mobileStrategy": "full",
    "reducedMotion": "The hamburger morph transition and mobile panel entrance animation are disabled entirely under `prefers-reduced-motion: reduce` (panel appears instantly, bars snap to their open/closed state with no transition). The desktop bar has no motion beyond an opacity/color hover on links and CTA, which is left in place as it is a state change, not decorative movement.",
    "performance": {
      "bundleCost": "~2.5KB gzip, zero runtime dependencies beyond React",
      "offscreenBehavior": "The mobile panel and its `inert`/focus-trap listeners only mount and attach while `open` is true; closed, the component is a static header with no document-level listeners.",
      "notes": "The nav bar itself does no per-frame work — the glass border/blur is pure CSS. The only imperative work is on menu open/close: querying focusable elements in the panel and toggling `inert` on body children, both O(n) in DOM children, not run on scroll or resize."
    },
    "a11y": "Nav is a labeled `<header><nav aria-label=\"Primary\">`. The mobile trigger button exposes `aria-expanded` and `aria-controls` pointing at the panel id. The open panel is a proper `role=\"dialog\" aria-modal=\"true\"` with an accessible name, receives initial focus on open, traps Tab/Shift+Tab within itself, closes and restores focus to the trigger on Escape or link activation, and applies `inert` to all other body children while open so background content is unreachable by keyboard or assistive tech.",
    "knownLimitations": [
      "The inert-based trap assumes the nav is a direct or ancestor-adjacent child of `<body>`; deeply portaled usage may need the inert targets adjusted.",
      "Backdrop-filter blur has no fallback for browsers that don't support it — the bar degrades to a semi-transparent flat panel, which is still legible but loses the glass effect.",
      "Only one instance of the mobile panel should be open at a time per page; the component does not coordinate with sibling instances."
    ],
    "license": "MIT",
    "origin": "New implementation for Safari Motion, following the cinematic-liquid-glass-nav pattern (gradient-mask border via mask-composite: exclude) and an inert + focus-trap mobile panel written from scratch for this registry.",
    "author": "Viktor Veverka (https://viktorveverka.cz)",
    "relatedComponents": [
      "hero-aurora"
    ]
  }
}