{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nav-side",
  "type": "registry:block",
  "title": "Side navigation",
  "description": "Side navigation for documentation, with collapsible groups and one named landmark. On small screens it becomes a drawer.",
  "categories": [
    "section"
  ],
  "dependencies": [
    "@base-ui-components/react@1.0.0-rc.0",
    "lucide-react@^1.47.0"
  ],
  "registryDependencies": [
    "https://facadeui.dev/r/button.json",
    "https://facadeui.dev/r/heading.json",
    "https://facadeui.dev/r/types.json",
    "https://facadeui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/sections/nav-side.tsx",
      "type": "registry:component",
      "target": "components/sections/nav-side.tsx",
      "content": "\"use client\"\n\n/**\n * NavSide — the docs- and product-style side navigation.\n *\n * Groups of links, optionally collapsible, with the current page marked. Below\n * `lg` it collapses into the same Base UI Dialog drawer pattern `NavTop` uses,\n * because a fixed sidebar on a phone is just lost width.\n *\n * a11y:\n *\n *  - One `<nav>` with an accessible name, and group headings inside it. Four\n *    separate `<nav>` landmarks — one per group — is the more common shape and\n *    the worse one: landmark navigation then lists four items called\n *    \"navigation\" with nothing to tell them apart.\n *  - Groups are real headings, so heading navigation works down the sidebar.\n *  - Collapsible groups use Base UI's Collapsible, so the trigger is a button\n *    inside the heading with `aria-expanded` and `aria-controls` wired up.\n *  - The current page carries `aria-current=\"page\"`, announced rather than only\n *    coloured. Its ancestor group is expanded on first render, so the active\n *    item is never hidden inside a collapsed section.\n *  - The sidebar scrolls independently and the drawer traps focus.\n *\n * Dependencies: @base-ui-components/react, lucide-react, react,\n * @/lib/types, @/lib/utils, @/components/ui/button,\n * @/components/ui/heading.\n */\n\nimport { Collapsible } from \"@base-ui-components/react/collapsible\"\nimport { Dialog } from \"@base-ui-components/react/dialog\"\nimport { ChevronDownIcon, MenuIcon, XIcon } from \"lucide-react\"\nimport { useState, type ElementType, type ReactNode } from \"react\"\n\nimport type { HeadingLevel, LinkComponent } from \"@/lib/types\"\nimport { cn, slugId } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\nimport { Heading } from \"@/components/ui/heading\"\n\nexport interface NavSideLink {\n  label: string\n  href: string\n  /** Small trailing tag — \"New\", \"Beta\". */\n  badge?: ReactNode\n  external?: boolean\n}\n\nexport interface NavSideGroup {\n  title: string\n  links: NavSideLink[]\n  /** Makes the group a Base UI Collapsible. */\n  collapsible?: boolean\n  /** Start collapsed. Ignored when the group holds the current page. */\n  defaultCollapsed?: boolean\n}\n\nexport interface NavSideProps {\n  groups: NavSideGroup[]\n  /** Names the landmark, e.g. \"Documentation\". */\n  label?: string\n  currentPath?: string\n  link?: LinkComponent\n  /** Heading level of the group headings. Defaults to `2`. */\n  headingLevel?: HeadingLevel\n  /** Shown above the groups, such as a version switcher or a search button. */\n  children?: ReactNode\n  /** Top offset of the sticky sidebar, so it clears a fixed header. */\n  stickyTop?: string\n  className?: string\n  id?: string\n}\n\nconst linkClass =\n  \"focus-visible:ring-ring flex min-h-9 items-center justify-between gap-2 rounded-md px-3 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n\nfunction NavLinks({\n  group,\n  currentPath,\n  link,\n  onNavigate,\n}: {\n  group: NavSideGroup\n  currentPath?: string\n  link?: LinkComponent\n  onNavigate?: () => void\n}) {\n  const Link = (link ?? \"a\") as ElementType\n\n  return (\n    <ul className=\"flex flex-col gap-0.5\">\n      {group.links.map((item) => {\n        const active = currentPath === item.href\n        return (\n          <li key={item.href}>\n            <Link\n              href={item.href}\n              onClick={onNavigate}\n              aria-current={active ? \"page\" : undefined}\n              {...(item.external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n              className={cn(\n                linkClass,\n                active\n                  ? \"bg-secondary text-secondary-foreground font-medium\"\n                  : \"text-muted-foreground hover:text-foreground hover:bg-accent/60\",\n              )}\n            >\n              <span>{item.label}</span>\n              {item.badge}\n              {item.external ? (\n                <span className=\"sr-only\"> (opens in a new tab)</span>\n              ) : null}\n            </Link>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nfunction NavGroups({\n  groups,\n  currentPath,\n  link,\n  headingLevel,\n  onNavigate,\n}: {\n  groups: NavSideGroup[]\n  currentPath?: string\n  link?: LinkComponent\n  headingLevel: HeadingLevel\n  onNavigate?: () => void\n}) {\n  return (\n    <div className=\"flex flex-col gap-7\">\n      {groups.map((group) => {\n        const holdsCurrent = group.links.some((item) => item.href === currentPath)\n        const headingId = slugId(group.title, \"nav\")\n\n        if (!group.collapsible) {\n          return (\n            <div key={group.title} className=\"flex flex-col gap-1.5\">\n              <Heading\n                level={headingLevel}\n                id={headingId}\n                className=\"text-foreground px-3 text-sm font-semibold\"\n              >\n                {group.title}\n              </Heading>\n              <NavLinks\n                group={group}\n                currentPath={currentPath}\n                link={link}\n                onNavigate={onNavigate}\n              />\n            </div>\n          )\n        }\n\n        return (\n          <Collapsible.Root\n            key={group.title}\n            // Never start collapsed around the page the user is on.\n            defaultOpen={holdsCurrent || !group.defaultCollapsed}\n            className=\"flex flex-col gap-1.5\"\n          >\n            <Heading level={headingLevel} id={headingId}>\n              <Collapsible.Trigger className=\"text-foreground hover:bg-accent/60 focus-visible:ring-ring group flex min-h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md px-3 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2\">\n                {group.title}\n                <ChevronDownIcon\n                  aria-hidden\n                  focusable=\"false\"\n                  className=\"text-muted-foreground duration-facade-fast ease-facade-out size-4 transition-transform group-data-[panel-open]:rotate-180\"\n                />\n              </Collapsible.Trigger>\n            </Heading>\n            <Collapsible.Panel className=\"duration-facade-base ease-facade-out h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] data-[ending-style]:h-0 data-[starting-style]:h-0\">\n              <NavLinks\n                group={group}\n                currentPath={currentPath}\n                link={link}\n                onNavigate={onNavigate}\n              />\n            </Collapsible.Panel>\n          </Collapsible.Root>\n        )\n      })}\n    </div>\n  )\n}\n\nexport function NavSide({\n  groups,\n  label = \"Section\",\n  currentPath,\n  link,\n  headingLevel = 2,\n  children,\n  stickyTop = \"5rem\",\n  className,\n  id,\n}: NavSideProps) {\n  const [drawerOpen, setDrawerOpen] = useState(false)\n\n  return (\n    <>\n      <nav id={id} aria-label={label} className={cn(\"hidden lg:block\", className)}>\n        <div\n          className=\"sticky overflow-y-auto pb-10 pr-4\"\n          style={{ top: stickyTop, maxHeight: `calc(100dvh - ${stickyTop} - 1rem)` }}\n        >\n          {children ? <div className=\"mb-6\">{children}</div> : null}\n          <NavGroups\n            groups={groups}\n            currentPath={currentPath}\n            link={link}\n            headingLevel={headingLevel}\n          />\n        </div>\n      </nav>\n\n      <Dialog.Root open={drawerOpen} onOpenChange={setDrawerOpen}>\n        <Dialog.Trigger\n          aria-label={`Open ${label.toLowerCase()} navigation`}\n          className={cn(buttonVariants({ variant: \"outline\", size: \"sm\" }), \"lg:hidden\")}\n        >\n          <MenuIcon aria-hidden focusable=\"false\" className=\"size-4\" />\n          {label}\n        </Dialog.Trigger>\n\n        <Dialog.Portal>\n          <Dialog.Backdrop className=\"duration-facade-base fixed inset-0 z-50 bg-black/50 transition-opacity data-[ending-style]:opacity-0 data-[starting-style]:opacity-0\" />\n          <Dialog.Popup className=\"bg-background duration-facade-base ease-facade-out fixed inset-y-0 left-0 z-50 flex w-[min(20rem,85vw)] flex-col gap-6 overflow-y-auto border-r p-6 transition-transform data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full\">\n            <div className=\"flex items-center justify-between gap-4\">\n              <Dialog.Title className=\"text-base font-semibold\">{label}</Dialog.Title>\n              <Dialog.Close\n                aria-label=\"Close navigation\"\n                className={buttonVariants({ variant: \"ghost\", size: \"icon\" })}\n              >\n                <XIcon aria-hidden focusable=\"false\" />\n              </Dialog.Close>\n            </div>\n            <Dialog.Description className=\"sr-only\">\n              Browse the pages in this section.\n            </Dialog.Description>\n\n            {children}\n\n            <nav aria-label={label}>\n              <NavGroups\n                groups={groups}\n                currentPath={currentPath}\n                link={link}\n                headingLevel={headingLevel}\n                onNavigate={() => setDrawerOpen(false)}\n              />\n            </nav>\n          </Dialog.Popup>\n        </Dialog.Portal>\n      </Dialog.Root>\n    </>\n  )\n}\n"
    }
  ],
  "docs": "Import styles/facade-tokens.css in your global stylesheet, after @import \"tailwindcss\". Set headingLevel to fit the page outline (sections default to h2). Pass next/link and next/image through the link and image props. Docs: https://facadeui.dev/components/nav-side"
}
