{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sidebar","type":"registry:ui","title":"Sidebar","description":"An app's side navigation: groups of links, sections that open, a sliding current-page highlight, and a collapse to icons.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/lib-use-indicator.json","https://dashral.com/r/dropdown-menu.json","https://dashral.com/r/tooltip.json"],"files":[{"path":"registry/dashral/sidebar.tsx","type":"registry:file","target":"components/dashral/sidebar.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRightIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { indicatorMotion, useIndicator } from \"./lib/use-indicator\"\nimport { cn } from \"./lib/utils\"\nimport { DropdownMenu } from \"./dropdown-menu\"\nimport { Tooltip } from \"./tooltip\"\n\n/** A place in the sidebar: a link (href), an action (onSelect), or a section that opens to more (children). */\ntype SidebarLink = {\n  type?: \"item\"\n  /** The name, shown beside the icon (and in a tooltip while the sidebar is collapsed). */\n  label: string\n  icon?: React.ReactNode\n  href?: string\n  onSelect?: () => void\n  /** The page you're on: highlighted and announced as the current page. */\n  active?: boolean\n  /** At the end of the row, e.g. a count or a \"New\" tag. Hidden while collapsed. */\n  badge?: React.ReactNode\n  disabled?: boolean\n  /** One level of links under this one, opened and closed by its row. */\n  children?: SidebarLink[]\n  /** With children: open to begin with (a section with the current page in it opens on its own). */\n  defaultOpen?: boolean\n}\n\ntype SidebarItem = SidebarLink | { type: \"group\"; label?: React.ReactNode; items: SidebarLink[] } | { type: \"separator\" }\n\ntype SidebarProps = Omit<React.ComponentProps<\"aside\">, \"children\"> & {\n  items: SidebarItem[]\n  /** At the top: a logo, a workspace switcher. Only its first icon-sized part shows while collapsed, so keep one there. */\n  header?: React.ReactNode\n  /** At the bottom: the signed-in user, a help link. */\n  footer?: React.ReactNode\n  /** Narrowed to its icons. Controlled; or set defaultCollapsed and let the toggle change it. */\n  collapsed?: boolean\n  defaultCollapsed?: boolean\n  onCollapsedChange?: (collapsed: boolean) => void\n  /** The collapse toggle at the bottom. On by default. */\n  collapsible?: boolean\n  /** Your router's link, for items with href. */\n  linkAs?: React.ElementType\n  /** Names the navigation for screen readers. */\n  \"aria-label\"?: string\n  classNames?: { header?: string; nav?: string; footer?: string; item?: string }\n}\n\n// The list's columns are minmax(0, 1fr) (grid-cols-1): a row can shrink below its label's width as the sidebar narrows.\n// The current row's background is the sliding highlight; until that's measured (no JavaScript yet), the row draws its own.\nconst rowClass =\n  \"relative z-10 flex w-full items-center gap-3 rounded-md px-3 py-2 text-[13px] font-medium text-sidebar-foreground outline-none transition-colors hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-[current=page]:text-sidebar-accent-foreground aria-[current=page]:hover:bg-transparent not-in-data-measured:data-sidebar-current:bg-sidebar-accent [&_svg]:size-4 [&_svg]:shrink-0\"\n\n/**\n * An app's side navigation in one tag: a header (your logo), groups of links with icons, badges and one level of\n * sections that open, and a footer (the signed-in user). The current page's highlight slides to it. Collapsed, it\n * narrows to its icons: each shows its name in a tooltip, and a section opens its links in a menu beside it. For phones,\n * put it in a Sheet.\n */\nfunction Sidebar({\n  items,\n  header,\n  footer,\n  collapsed: collapsedProp,\n  defaultCollapsed = false,\n  onCollapsedChange,\n  collapsible = true,\n  linkAs = \"a\",\n  \"aria-label\": ariaLabel,\n  className,\n  classNames,\n  ...props\n}: SidebarProps) {\n  const { t, dir } = useLocale()\n  const [own, setOwn] = React.useState(defaultCollapsed)\n  const collapsed = collapsedProp ?? own\n  const toggle = () => {\n    if (collapsedProp === undefined) setOwn(!collapsed)\n    onCollapsedChange?.(!collapsed)\n  }\n  // The current page's highlight: one element sliding to whichever row is marked current.\n  const [nav, rect] = useIndicator<HTMLDivElement>(\"[data-sidebar-current]\")\n  const side = dir === \"rtl\" ? \"left\" : \"right\"\n  const Toggle = collapsed !== (dir === \"rtl\") ? PanelLeftOpenIcon : PanelLeftCloseIcon\n\n  return (\n    <aside\n      data-slot=\"sidebar\"\n      data-collapsed={collapsed || undefined}\n      className={cn(\n        \"flex h-full shrink-0 flex-col border-e border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-(--duration-considered) ease-standard motion-reduce:transition-none\",\n        collapsed ? \"w-15\" : \"w-60\",\n        className\n      )}\n      {...props}\n    >\n      {header && (\n        // Narrowed, only its first 28px show (a logo's mark), right where the icons below sit; the rest is clipped away.\n        <div className={cn(\"flex h-14 shrink-0 items-center ps-4 pe-3\", classNames?.header)}>\n          <div\n            className={cn(\n              \"flex min-w-0 items-center overflow-hidden transition-[max-width] duration-(--duration-considered) ease-standard motion-reduce:transition-none\",\n              collapsed ? \"max-w-7\" : \"max-w-52\"\n            )}\n          >\n            {header}\n          </div>\n        </div>\n      )}\n      <nav aria-label={ariaLabel ?? t(\"navigation\")} className={cn(\"min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-2 py-2\", classNames?.nav)}>\n        <div ref={nav} className=\"relative grid grid-cols-1 gap-0.5\" data-measured={rect ? \"\" : undefined}>\n          {rect && (\n            <span\n              aria-hidden\n              className={cn(indicatorMotion, \"rounded-md bg-sidebar-accent\")}\n              style={{ translate: `${rect.x}px ${rect.y}px`, width: rect.width, height: rect.height }}\n            />\n          )}\n          {items.map((item, i) =>\n            item.type === \"separator\" ? (\n              <hr key={i} className=\"my-2 border-sidebar-border\" />\n            ) : item.type === \"group\" ? (\n              <div key={i} role=\"group\" aria-label={typeof item.label === \"string\" ? item.label : undefined} className=\"mt-3 grid grid-cols-1 gap-0.5 first:mt-0\">\n                {item.label &&\n                  (collapsed ? (\n                    <hr className=\"mx-2 mb-1 border-sidebar-border\" />\n                  ) : (\n                    <p className=\"truncate px-3 pb-1 text-[11px] font-medium tracking-wide text-muted-foreground uppercase\">{item.label}</p>\n                  ))}\n                {item.items.map((link, j) => (\n                  <SidebarRow key={j} link={link} collapsed={collapsed} side={side} linkAs={linkAs} className={classNames?.item} />\n                ))}\n              </div>\n            ) : (\n              <SidebarRow key={i} link={item} collapsed={collapsed} side={side} linkAs={linkAs} className={classNames?.item} />\n            )\n          )}\n        </div>\n      </nav>\n      {(footer || collapsible) && (\n        <div className={cn(\"grid shrink-0 grid-cols-1 gap-2 border-t border-sidebar-border p-2\", classNames?.footer)}>\n          {/* Clipped to the row's width, so folded only what's in line with the icons shows (an avatar, say). */}\n          {footer && <div className=\"min-w-0 overflow-hidden\">{footer}</div>}\n          {collapsible && (\n            <Tooltip content={collapsed ? t(\"expandSidebar\") : t(\"collapseSidebar\")} side={side}>\n              <button\n                type=\"button\"\n                onClick={toggle}\n                aria-label={collapsed ? t(\"expandSidebar\") : t(\"collapseSidebar\")}\n                aria-expanded={!collapsed}\n                className={cn(rowClass, \"text-muted-foreground\", collapsed && \"justify-center px-0\")}\n              >\n                <Toggle aria-hidden />\n                <span className={cn(\"truncate\", collapsed && \"sr-only\")}>{collapsed ? t(\"expandSidebar\") : t(\"collapseSidebar\")}</span>\n              </button>\n            </Tooltip>\n          )}\n        </div>\n      )}\n    </aside>\n  )\n}\n\n/** A row: a link or action, or a section's toggle with its links under it. */\nfunction SidebarRow({\n  link,\n  collapsed,\n  side,\n  linkAs: Link,\n  className,\n}: {\n  link: SidebarLink\n  collapsed: boolean\n  side: \"left\" | \"right\"\n  linkAs: React.ElementType\n  className?: string\n}) {\n  const childActive = link.children?.some((child) => child.active) ?? false\n  const [open, setOpen] = React.useState(link.defaultOpen ?? childActive)\n  // A section with the current page in it opens when you arrive there.\n  const [wasActive, setWasActive] = React.useState(childActive)\n  if (wasActive !== childActive) {\n    setWasActive(childActive)\n    if (childActive) setOpen(true)\n  }\n  const sectionId = React.useId()\n\n  const inner = (\n    <>\n      {link.icon}\n      <span className={cn(\"min-w-0 flex-1 truncate text-start\", collapsed && \"sr-only\")}>{link.label}</span>\n      {link.badge && !collapsed && <span className=\"shrink-0\">{link.badge}</span>}\n    </>\n  )\n\n  if (link.children) {\n    if (collapsed) {\n      // Narrowed: the section's links open in a menu beside it.\n      return (\n        <DropdownMenu\n          side={side}\n          align=\"start\"\n          className=\"min-w-44\"\n          linkAs={Link}\n          items={[\n            { type: \"label\", label: link.label },\n            ...link.children.map((child) => ({ label: child.label, icon: child.icon, href: child.href, active: child.active, disabled: child.disabled, onSelect: child.onSelect })),\n          ]}\n          trigger={\n            <button\n              type=\"button\"\n              aria-label={link.label}\n              disabled={link.disabled}\n              data-sidebar-current={childActive || undefined}\n              className={cn(rowClass, \"justify-center px-0\", className)}\n            >\n              {link.icon}\n            </button>\n          }\n        />\n      )\n    }\n    return (\n      <div>\n        <button\n          type=\"button\"\n          onClick={() => setOpen(!open)}\n          aria-expanded={open}\n          aria-controls={sectionId}\n          disabled={link.disabled}\n          // Closed with the current page inside, the section's own row carries the highlight.\n          data-sidebar-current={(childActive && !open) || undefined}\n          className={cn(rowClass, className)}\n        >\n          {inner}\n          <ChevronRightIcon aria-hidden className={cn(\"transition-transform duration-(--duration-standard) ease-standard rtl:-scale-x-100\", open && \"rotate-90 rtl:rotate-90\")} />\n        </button>\n        {/* Height by grid rows, so it opens and closes without measuring; closed, it's inert. */}\n        <div\n          id={sectionId}\n          inert={!open}\n          className={cn(\n            \"grid transition-[grid-template-rows,opacity] duration-(--duration-standard) ease-standard motion-reduce:transition-none\",\n            open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\"\n          )}\n        >\n          <div className=\"min-h-0 overflow-hidden\">\n            <div className=\"ms-5 grid grid-cols-1 gap-0.5 border-s border-sidebar-border py-0.5 ps-2\">\n              {link.children.map((child, i) => (\n                <SidebarRow key={i} link={child} collapsed={false} side={side} linkAs={Link} className={cn(\"py-1.5\", className)} />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  const row = link.href ? (\n    <Link\n      href={link.href}\n      aria-current={link.active ? \"page\" : undefined}\n      aria-disabled={link.disabled || undefined}\n      data-sidebar-current={link.active || undefined}\n      onClick={link.onSelect}\n      className={cn(rowClass, collapsed && \"justify-center px-0\", link.disabled && \"pointer-events-none opacity-50\", className)}\n    >\n      {inner}\n    </Link>\n  ) : (\n    <button\n      type=\"button\"\n      onClick={link.onSelect}\n      disabled={link.disabled}\n      data-sidebar-current={link.active || undefined}\n      aria-current={link.active ? \"page\" : undefined}\n      className={cn(rowClass, collapsed && \"justify-center px-0\", className)}\n    >\n      {inner}\n    </button>\n  )\n\n  // Narrowed to its icon, a row says its name in a tooltip beside it.\n  return collapsed ? (\n    <Tooltip content={link.label} side={side}>\n      {row}\n    </Tooltip>\n  ) : (\n    row\n  )\n}\n\nexport { Sidebar, type SidebarProps, type SidebarItem, type SidebarLink }\n"}],"cssVars":{"theme":{"ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{}}}