{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-sidebar-2","type":"registry:block","title":"Icon rail that collapses","description":"Labels give way to tooltips at 3rem, with a rail and a trigger to drive it.","dependencies":["cn"],"registryDependencies":["avatar","button","sidebar"],"files":[{"path":"c-sidebar-2.tsx","type":"registry:block","content":"\"use client\"\n\nimport type { ReactNode } from \"react\"\nimport { cn } from \"cn\"\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarTrigger,\n} from \"@/components/ui/sidebar\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface RailItem {\n  id: string\n  label: string\n  icon: ReactNode\n  badge?: number\n  isActive?: boolean\n}\n\ninterface Monitor {\n  id: string\n  label: string\n  value: string\n  icon: ReactNode\n}\n\n// Each icon is a JSX node whose per-library names are STATIC literals. That is\n// the shape `shadcn add` rewrites when it swaps the icon package, so an\n// interpolated value would leave it nothing to rewrite.\nconst NAV: RailItem[] = [\n  {\n    id: \"overview\",\n    label: \"Overview\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"LayoutDashboardIcon\"\n        tabler=\"IconLayoutDashboard\"\n        hugeicons=\"DashboardSquare02Icon\"\n        phosphor=\"LayoutIcon\"\n        remixicon=\"RiDashboardLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"inbox\",\n    label: \"Inbox\",\n    badge: 12,\n    icon: (\n      <IconPlaceholder\n        lucide=\"InboxIcon\"\n        tabler=\"IconInbox\"\n        hugeicons=\"InboxIcon\"\n        phosphor=\"TrayIcon\"\n        remixicon=\"RiInboxLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"deployments\",\n    label: \"Deployments\",\n    isActive: true,\n    icon: (\n      <IconPlaceholder\n        lucide=\"RocketIcon\"\n        tabler=\"IconRocket\"\n        hugeicons=\"RocketIcon\"\n        phosphor=\"RocketIcon\"\n        remixicon=\"RiRocketLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"monitoring\",\n    label: \"Monitoring\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"ActivityIcon\"\n        tabler=\"IconActivity\"\n        hugeicons=\"Pulse01Icon\"\n        phosphor=\"ActivityIcon\"\n        remixicon=\"RiPulseLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"analytics\",\n    label: \"Analytics\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"BarChart3Icon\"\n        tabler=\"IconChartBar\"\n        hugeicons=\"Analytics01Icon\"\n        phosphor=\"ChartBarIcon\"\n        remixicon=\"RiBarChartLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"audit\",\n    label: \"Audit log\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"ShieldCheckIcon\"\n        tabler=\"IconShieldCheck\"\n        hugeicons=\"Shield01Icon\"\n        phosphor=\"ShieldCheckIcon\"\n        remixicon=\"RiShieldCheckLine\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"settings\",\n    label: \"Settings\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"SettingsIcon\"\n        tabler=\"IconSettings\"\n        hugeicons=\"SettingsIcon\"\n        phosphor=\"GearIcon\"\n        remixicon=\"RiSettings3Line\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n]\n\nconst MONITORS: Monitor[] = [\n  {\n    id: \"edge\",\n    label: \"Edge network\",\n    value: \"99.99%\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"GlobeIcon\"\n        tabler=\"IconWorld\"\n        hugeicons=\"Globe02Icon\"\n        phosphor=\"GlobeIcon\"\n        remixicon=\"RiGlobalLine\"\n        className=\"text-muted-foreground size-4 shrink-0\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"webhooks\",\n    label: \"Webhook delivery\",\n    value: \"42ms p95\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"ZapIcon\"\n        tabler=\"IconBolt\"\n        hugeicons=\"FlashIcon\"\n        phosphor=\"LightningIcon\"\n        remixicon=\"RiFlashlightLine\"\n        className=\"text-muted-foreground size-4 shrink-0\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n  {\n    id: \"jobs\",\n    label: \"Background jobs\",\n    value: \"12 queued\",\n    icon: (\n      <IconPlaceholder\n        lucide=\"DatabaseIcon\"\n        tabler=\"IconDatabase\"\n        hugeicons=\"Database02Icon\"\n        phosphor=\"DatabaseIcon\"\n        remixicon=\"RiDatabase2Line\"\n        className=\"text-muted-foreground size-4 shrink-0\"\n        aria-hidden=\"true\"\n      />\n    ),\n  },\n]\n\n// The rail itself. Two things the primitive already handles, so nothing here\n// has to: the button clips its own label at a 3rem width instead of removing\n// it, so the accessible name survives the collapse, and SidebarMenuBadge\n// carries `group-data-[collapsible=icon]:hidden`, so counts disappear with the\n// labels rather than stacking on top of an icon.\n//\n// `tooltip` is the other half of that bargain. The primitive renders it with\n// `hidden` until `state` is collapsed, so it costs nothing while the labels\n// are on screen and becomes the only name a mouse user sees once they are not.\nfunction RailNav() {\n  return (\n    <SidebarGroup>\n      <SidebarGroupLabel>Platform</SidebarGroupLabel>\n      <SidebarGroupContent>\n        <SidebarMenu>\n          {NAV.map((item) => (\n            <SidebarMenuItem key={item.id}>\n              <SidebarMenuButton\n                render={<a href=\"#\" />}\n                isActive={item.isActive}\n                tooltip={item.label}\n              >\n                {item.icon}\n                <span>{item.label}</span>\n              </SidebarMenuButton>\n              {item.badge ? (\n                <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>\n              ) : null}\n            </SidebarMenuItem>\n          ))}\n        </SidebarMenu>\n      </SidebarGroupContent>\n    </SidebarGroup>\n  )\n}\n\n// The ReUI mark, the same path data the App Shell Pro blocks use. An inline\n// SVG on `currentColor` rather than an <img> or an icon-package import: it\n// ships inside the example, needs no asset and recolours with the panel.\nfunction BrandMark({ className }: { className?: string }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"bg-sidebar-primary text-sidebar-primary-foreground flex size-6 shrink-0 items-center justify-center rounded-md\",\n        className\n      )}\n    >\n      <svg\n        viewBox=\"25.5002 25.1352 50 50\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        className=\"size-3\"\n      >\n        <circle cx=\"70.634\" cy=\"29.8334\" r=\"4.69799\" fill=\"currentColor\" />\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M25.668 57.0144V29.8332C25.668 27.2386 27.7713 25.1352 30.366 25.1352C32.9606 25.1352 35.0639 27.2386 35.0639 29.8332V57.0144C35.0639 61.833 38.9702 65.7392 43.7888 65.7392H57.2116C62.0302 65.7392 65.9364 61.833 65.9364 57.0144V43.7258C65.9364 41.1312 68.0398 39.0278 70.6344 39.0278C73.229 39.0278 75.3324 41.1312 75.3324 43.7258V57.0144C75.3324 67.0222 67.2194 75.1352 57.2116 75.1352H43.7888C33.7809 75.1352 25.668 67.0222 25.668 57.0144Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    </span>\n  )\n}\n\n// The mark is BrandMark, the same inline SVG the other sidebar examples carry,\n// scaled at the call site from its 24px default to the 32px tile sidebar-08\n// gives a `size=\"lg\"` header row.\n//\n// `size=\"lg\"` is what makes that tile survive the collapse: every menu button\n// is forced to `size-8` on the rail, and large is the one size that zeroes the\n// padding there in all eight styles, so a 32px tile fills it exactly.\n// `group-data-[collapsible=icon]:sr-only` on the name block is the other half.\n// It leaves the flex line, and takes the 8px gap with it, while keeping the\n// button's accessible name, so the tile is the only child left and sits on the\n// rail's axis with nothing needed to centre it.\nfunction BrandButton() {\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <SidebarMenuButton\n          size=\"lg\"\n          tooltip=\"ReUI Labs\"\n          render={<a href=\"#\" />}\n        >\n          <BrandMark className=\"size-8 rounded-lg [&>svg]:size-4\" />\n          <span className=\"grid min-w-0 flex-1 text-left leading-tight group-data-[collapsible=icon]:sr-only\">\n            <span className=\"truncate text-sm font-medium\">ReUI Labs</span>\n            <span className=\"text-sidebar-foreground/70 truncate text-xs\">\n              Production\n            </span>\n          </span>\n        </SidebarMenuButton>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  )\n}\n\n// The same collapsed-rail treatment as the brand mark, with the avatar sized\n// to match it. Avatar's default is 32px, which is both what sidebar-07's\n// footer row uses and what fills the collapsed button edge to edge.\nfunction AccountButton() {\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <SidebarMenuButton\n          size=\"lg\"\n          tooltip=\"Nadia Rahman\"\n          render={<a href=\"#\" />}\n        >\n          <Avatar>\n            <AvatarImage src=\"https://github.com/shadcn.png\" alt=\"\" />\n            <AvatarFallback>NR</AvatarFallback>\n          </Avatar>\n          <span className=\"grid min-w-0 flex-1 text-left leading-tight group-data-[collapsible=icon]:sr-only\">\n            <span className=\"truncate text-sm font-medium\">Nadia Rahman</span>\n            <span className=\"text-sidebar-foreground/70 truncate text-xs\">\n              Owner\n            </span>\n          </span>\n        </SidebarMenuButton>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  )\n}\n\n// A rail that collapses to icons, and the two controls that drive it.\n//\n// The collapsed width is the provider's own `--sidebar-width-icon` (3rem), and\n// the open state lives in SidebarProvider, so nothing here mirrors it: both\n// controls call the same context.\n//\n// SidebarTrigger sits in the inset rather than in the panel because it is the\n// only control that survives both branches. shadcn's Sidebar renders its\n// desktop rail behind `hidden md:block` and swaps to a Sheet under 768px, and\n// SidebarRail is `sm:flex` against that desktop wrapper, so on a narrow frame\n// the trigger in the header is what opens the navigation at all.\n//\n// `min-h-0` beside the height is load-bearing. SidebarProvider ships\n// `min-h-svh`, and tailwind-merge only drops it when another `min-h-*` arrives\n// with it, so without the pair this shell is viewport tall wherever it lands.\n//\n// `absolute h-full` on the Sidebar is the other half of that. The desktop\n// branch's panel ships `fixed inset-y-0 h-svh`, and a fixed box ignores both\n// the height above and the ancestor's overflow, so it would size and position\n// against the VIEWPORT and paint down the page beside a 468px shell. The\n// className lands on that container and tailwind-merge swaps both groups, and\n// `relative` on the provider is what the panel then measures itself against.\n// The mobile branch is untouched: it renders a Sheet and never forwards\n// className to it.\nexport function Pattern() {\n  return (\n    <SidebarProvider className=\"relative h-dvh min-h-0 w-full overflow-hidden\">\n      <Sidebar collapsible=\"icon\" className=\"absolute h-full\">\n        <SidebarHeader>\n          <BrandButton />\n        </SidebarHeader>\n\n        <SidebarContent role=\"navigation\" aria-label=\"Primary\">\n          <RailNav />\n        </SidebarContent>\n\n        <SidebarFooter>\n          <AccountButton />\n        </SidebarFooter>\n\n        {/*\n          SidebarRail ships its own `tabIndex={-1}` and `aria-label`, so the\n          grab strip on the panel edge is a mouse affordance and the trigger in\n          the inset header is the keyboard path. Two tab stops for one toggle\n          would only slow a keyboard user down.\n        */}\n        <SidebarRail />\n      </Sidebar>\n\n      <SidebarInset className=\"min-w-0 overflow-hidden\">\n        <header className=\"flex h-12 shrink-0 items-center gap-2 border-b px-3\">\n          <SidebarTrigger />\n          <h2 className=\"truncate text-sm font-medium\">Deployments</h2>\n          <Button variant=\"outline\" size=\"sm\" className=\"ml-auto\">\n            New deploy\n          </Button>\n        </header>\n\n        <div className=\"min-w-0 flex-1 overflow-auto p-4\">\n          <p className=\"text-muted-foreground max-w-prose text-sm\">\n            Every deploy runs against the edge network first. Collapse the rail\n            with the panel button or the strip on its edge, and the labels give\n            way to tooltips.\n          </p>\n\n          <dl className=\"mt-4 flex flex-col gap-3\">\n            {MONITORS.map((monitor) => (\n              <div key={monitor.id} className=\"flex items-center gap-2 text-sm\">\n                <dt className=\"flex min-w-0 flex-1 items-center gap-2\">\n                  {monitor.icon}\n                  <span className=\"truncate\">{monitor.label}</span>\n                </dt>\n                <dd className=\"shrink-0 tabular-nums\">{monitor.value}</dd>\n              </div>\n            ))}\n          </dl>\n        </div>\n      </SidebarInset>\n    </SidebarProvider>\n  )\n}","target":"components/examples/c-sidebar-2.tsx"}],"meta":{"order":2,"gridSize":1,"previewHeight":"560"}}