{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nav-site-condense",
  "title": "Nav Site Condense",
  "description": "A full-width site nav with the furniture a real site needs: a scroll-condensed bar, a menu trigger present at every width, and a mobile sheet built on native <dialog> for a free focus trap and Escape-to-close.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/nav-site-condense/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// NavSiteCondense — a full-width top nav with the mechanics an actual site\n// needs and this registry didn't have yet: real scroll state (a plain\n// threshold class-swap, deliberately subordinate — nav-condense-rail already\n// owns the continuous, measured density transition and header-scroll-pill\n// already owns the silhouette morph into a pill; this component's job is\n// everything neither of those ships), a menu trigger that is present at\n// every width (not hidden behind a responsive breakpoint that would make it\n// unreachable at some viewport sizes), and a mobile sheet built on the\n// native <dialog> element for a free focus trap, Escape-to-close and\n// top-layer stacking.\n// ---------------------------------------------------------------------------\n\nexport interface NavLinkItem {\n  label: string;\n  href: string;\n}\n\nexport interface NavSiteCondenseProps {\n  /** wordmark / brand text */\n  brand?: string;\n  /** nav links, in order */\n  links: NavLinkItem[];\n  /** px of scrollY past which the bar condenses. default 24 */\n  condenseAt?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function NavSiteCondense({\n  brand = \"ns-ui\",\n  links,\n  condenseAt = 24,\n  className = \"\",\n}: NavSiteCondenseProps) {\n  const [condensed, setCondensed] = useState(false);\n  const [sheetOpen, setSheetOpen] = useState(false);\n\n  const dialogRef = useRef<HTMLDialogElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const dialogId = useId();\n  const sheetTitleId = useId();\n\n  // -- scroll condense: a plain threshold, not a continuous interpolation --\n  useEffect(() => {\n    let ticking = false;\n    const evaluate = () => {\n      ticking = false;\n      setCondensed(window.scrollY > condenseAt);\n    };\n    const onScroll = () => {\n      if (ticking) return;\n      ticking = true;\n      requestAnimationFrame(evaluate);\n    };\n    evaluate(); // honest initial read: the page may load already scrolled\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => window.removeEventListener(\"scroll\", onScroll);\n  }, [condenseAt]);\n\n  // -- sheet: native <dialog> drives the trap, Escape and top layer --------\n  useEffect(() => {\n    const dlg = dialogRef.current;\n    if (!dlg) return;\n\n    if (sheetOpen && !dlg.open) dlg.showModal();\n    if (!sheetOpen && dlg.open) dlg.close();\n\n    const onCancel = (e: Event) => {\n      // native Escape handling; just keep our state in sync\n      e.preventDefault();\n      setSheetOpen(false);\n    };\n    const onClose = () => setSheetOpen(false);\n    const onBackdrop = (e: MouseEvent) => {\n      if (e.target === dlg) setSheetOpen(false);\n    };\n    dlg.addEventListener(\"cancel\", onCancel);\n    dlg.addEventListener(\"close\", onClose);\n    dlg.addEventListener(\"click\", onBackdrop);\n    return () => {\n      dlg.removeEventListener(\"cancel\", onCancel);\n      dlg.removeEventListener(\"close\", onClose);\n      dlg.removeEventListener(\"click\", onBackdrop);\n    };\n  }, [sheetOpen]);\n\n  const closeSheet = () => setSheetOpen(false);\n\n  return (\n    <>\n      <header\n        data-nav-site-condense\n        className={`sticky top-0 z-40 border-b transition-[padding-block,box-shadow,background-color] duration-300 motion-reduce:transition-none ${\n          condensed\n            ? \"border-border bg-background/95 py-2 shadow-sm backdrop-blur-sm\"\n            : \"border-transparent bg-background py-5\"\n        } ${className}`}\n      >\n        <div className=\"mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 sm:px-6\">\n          <a\n            href=\"#top\"\n            className={`shrink-0 rounded-sm font-mono font-semibold tracking-tight text-foreground transition-[font-size,color] duration-300 hover:text-ns-accent focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ns-accent motion-reduce:transition-none ${\n              condensed ? \"text-sm\" : \"text-base\"\n            }`}\n          >\n            {brand}\n          </a>\n\n          <nav aria-label=\"Primary\" className=\"hidden items-center gap-6 sm:flex\">\n            {links.map((link) => (\n              <a\n                key={link.href}\n                href={link.href}\n                className=\"rounded-sm text-sm text-ns-muted transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ns-accent\"\n              >\n                {link.label}\n              </a>\n            ))}\n          </nav>\n\n          {/* Present at every width on purpose: a trigger hidden behind a\n              responsive breakpoint is unreachable at whatever viewport a\n              test (or a user on an in-between width) actually has. Doubles\n              as the full-sitemap entry point even on wide screens. */}\n          <button\n            type=\"button\"\n            ref={triggerRef}\n            aria-haspopup=\"dialog\"\n            aria-controls={dialogId}\n            aria-expanded={sheetOpen}\n            aria-label=\"Open menu\"\n            onClick={() => setSheetOpen(true)}\n            className=\"inline-flex shrink-0 items-center justify-center gap-1.5 rounded-sm border border-border px-2 py-1.5 font-mono text-xs text-foreground transition-colors hover:border-foreground/25 hover:text-ns-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            <span aria-hidden className=\"leading-none\">\n              [≡]\n            </span>\n          </button>\n        </div>\n      </header>\n\n      <dialog\n        ref={dialogRef}\n        id={dialogId}\n        aria-modal=\"true\"\n        aria-labelledby={sheetTitleId}\n        className=\"ns-nav-sheet m-0 h-dvh max-h-none w-[min(20rem,88vw)] max-w-none overflow-hidden border-l border-border bg-surface p-0 text-foreground shadow-2xl\"\n      >\n        <div className=\"flex h-full flex-col\">\n          <div className=\"flex items-center justify-between border-b border-border px-4 py-4\">\n            <h2 id={sheetTitleId} className=\"font-mono text-sm font-semibold text-foreground\">\n              {brand}\n            </h2>\n            <button\n              type=\"button\"\n              onClick={closeSheet}\n              aria-label=\"Close menu\"\n              className=\"rounded-sm border border-border p-1.5 text-ns-muted transition-colors hover:border-foreground/25 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n            >\n              <svg aria-hidden viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\">\n                <path d=\"M4 4l8 8M12 4l-8 8\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n              </svg>\n            </button>\n          </div>\n          <nav aria-label=\"Mobile\" data-nav-site-condense-sheet className=\"flex flex-1 flex-col overflow-y-auto p-2\">\n            {links.map((link) => (\n              <a\n                key={link.href}\n                href={link.href}\n                onClick={closeSheet}\n                className=\"rounded-sm px-3 py-3 font-mono text-sm text-foreground transition-colors hover:bg-background focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n              >\n                {link.label}\n              </a>\n            ))}\n          </nav>\n        </div>\n\n        <style>{`\n          .ns-nav-sheet {\n            position: fixed;\n            inset: 0 0 0 auto;\n            transform: translateX(100%);\n            transition: transform 260ms ease, opacity 260ms ease;\n            opacity: 0;\n          }\n          .ns-nav-sheet[open] {\n            transform: translateX(0%);\n            opacity: 1;\n          }\n          .ns-nav-sheet::backdrop {\n            background: color-mix(in srgb, var(--foreground) 30%, transparent);\n          }\n          @media (prefers-reduced-motion: reduce) {\n            .ns-nav-sheet { transition: none; }\n          }\n        `}</style>\n      </dialog>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/nav-site-condense.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "nav",
      "header",
      "navigation",
      "sheet",
      "dialog",
      "scroll",
      "mobile",
      "focus-trap"
    ],
    "instruction": "Build <NavSiteCondense brand? links condenseAt? className?> where links is NavLinkItem[] ({label, href}). STRUCTURE: a sticky (position:sticky, top:0, z-40) <header data-nav-site-condense> containing a max-width row with a wordmark <a>, a hidden-below-sm inline link row (role via a real <nav aria-label=\"Primary\">), and a menu trigger <button aria-haspopup=\"dialog\" aria-controls aria-expanded aria-label=\"Open menu\">. CONDENSE: deliberately minimal and subordinate to the sheet — a passive, rAF-throttled scroll listener compares window.scrollY against condenseAt (default 24px) and flips one boolean React state; past the threshold the header swaps to tighter vertical padding, a background-color/backdrop-blur and a border-bottom, and the wordmark's font-size drops a step, all via a plain Tailwind class swap with a CSS transition (motion-reduce:transition-none), not a continuous interpolation, a measured height, or a spring — nav-condense-rail already owns that continuous, measured density transition and header-scroll-pill already owns the silhouette-morph-into-a-pill; duplicating either here would be a restyle. The initial scroll position is read once on mount so a page that loads already scrolled starts in the correct state. TRIGGER: rendered unconditionally at every viewport width, never hidden behind a `md:hidden`/`sm:hidden` breakpoint — a trigger that only exists below some breakpoint is unreachable (and unclickable by anything driving the page, human or otherwise) at any width at or above it. It doubles as a full-sitemap entry point even next to the inline desktop links. SHEET: a right-anchored panel built on the native <dialog> element. Opening sets React state to true, an effect calls dialogRef.current.showModal() (never .show() — modal is what gives the free focus trap, Escape-as-cancel and top-layer stacking), closing calls .close(). The panel's own CSS transitions transform from translateX(100%) to translateX(0%) and opacity 0 to 1 keyed off the dialog's own `[open]` attribute, with @media (prefers-reduced-motion: reduce) dropping the transition entirely. Backdrop click (event target === the dialog element itself, since ::backdrop isn't a real element) and Escape (the dialog's native `cancel` event, preventDefault'd only to keep this component's own state in sync, not to block the close) both close it, and the `close` event is also listened to so state never drifts out of sync with reality. Every link inside the sheet closes it on click. The sheet's own <nav aria-label=\"Mobile\" data-nav-site-condense-sheet> holds the link list. ACCESSIBILITY: the header's inline links and the sheet's links both carry hover (text-ns-muted to text-foreground) and focus-visible:outline-2 outline-offset-4 outline-ns-accent states with no outline-none on the same element (Tailwind v4 latches --tw-outline-style to none permanently if both are present, and the ring silently never paints even though the classes look correct). The dialog carries aria-modal=\"true\" and aria-labelledby pointing at a visible heading; Escape and the native focus trap are inherited for free from showModal(). Demo is a real, tall page with no scripted auto-scroll so /preview stays the honest interactive reference, while the card's autoplay:scroll descriptor eases the iframe a short way past the condense threshold."
  },
  "type": "registry:ui"
}