{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sidebar",
  "title": "Sidebar",
  "description": "Composable application sidebar with provider, groups, menus, sub-menus, rail, user row and a mobile sheet.",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.27.0",
    "radix-ui@^1.6.7"
  ],
  "registryDependencies": [
    "@pestoui/button",
    "@pestoui/collapsible",
    "@pestoui/input",
    "@pestoui/popover",
    "@pestoui/separator",
    "@pestoui/sheet",
    "@pestoui/skeleton",
    "@pestoui/style",
    "@pestoui/tooltip",
    "@pestoui/use-mobile",
    "@pestoui/utils"
  ],
  "files": [
    {
      "path": "pestoui/ui/sidebar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { useIsMobile } from \"@/hooks/use-mobile\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n} from \"@/components/ui/sheet\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { ChevronRightIcon, PanelLeftIcon } from \"lucide-react\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\ntype SidebarContextProps = {\n  state: \"expanded\" | \"collapsed\"\n  open: boolean\n  setOpen: (open: boolean) => void\n  openMobile: boolean\n  setOpenMobile: (open: boolean) => void\n  isMobile: boolean\n  toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\n/* True when the event landed in something the user is typing into. Shared by\n   the provider's shortcut and by any second shortcut a consumer layers on top\n   (⌘1…⌘4 team hints and the like) so they all opt out of the same places. */\nfunction isEditableTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) {\n    return false\n  }\n  if (target.isContentEditable) {\n    return true\n  }\n  const tag = target.tagName\n  return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\"\n}\n\nfunction useSidebar() {\n  const context = React.useContext(SidebarContext)\n  if (!context) {\n    throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n  }\n\n  return context\n}\n\nfunction SidebarProvider({\n  defaultOpen = true,\n  open: openProp,\n  onOpenChange: setOpenProp,\n  className,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  defaultOpen?: boolean\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n}) {\n  const isMobile = useIsMobile()\n  const [openMobile, setOpenMobile] = React.useState(false)\n\n  // This is the internal state of the sidebar.\n  // We use openProp and setOpenProp for control from outside the component.\n  const [_open, _setOpen] = React.useState(defaultOpen)\n  const open = openProp ?? _open\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value\n      if (setOpenProp) {\n        setOpenProp(openState)\n      } else {\n        _setOpen(openState)\n      }\n\n      // This sets the cookie to keep the sidebar state. It holds only an\n      // open/closed flag; SameSite and, over HTTPS, Secure are hygiene.\n      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax${window.location.protocol === \"https:\" ? \"; Secure\" : \"\"}`\n    },\n    [setOpenProp, open]\n  )\n\n  // Helper to toggle the sidebar.\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)\n  }, [isMobile, setOpen, setOpenMobile])\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        // The listener is on `window`, so it also sees keystrokes aimed at a\n        // text field — including SidebarInput, which lives inside the sidebar\n        // this very shortcut collapses. Typing must win: in a rich-text field\n        // the combination already means \"bold\", and stealing it there is a\n        // silent data-entry bug rather than a navigation shortcut.\n        if (isEditableTarget(event.target)) {\n          return\n        }\n        event.preventDefault()\n        toggleSidebar()\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown)\n    return () => window.removeEventListener(\"keydown\", handleKeyDown)\n  }, [toggleSidebar])\n\n  // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n  // This makes it easier to style the sidebar with Tailwind classes.\n  const state = open ? \"expanded\" : \"collapsed\"\n\n  const contextValue = React.useMemo<SidebarContextProps>(\n    () => ({\n      state,\n      open,\n      setOpen,\n      isMobile,\n      openMobile,\n      setOpenMobile,\n      toggleSidebar,\n    }),\n    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n  )\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <div\n        data-slot=\"sidebar-wrapper\"\n        style={\n          {\n            \"--sidebar-width\": SIDEBAR_WIDTH,\n            \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n            ...style,\n          } as React.CSSProperties\n        }\n        className={cn(\n          \"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </SidebarContext.Provider>\n  )\n}\n\nfunction Sidebar({\n  side = \"left\",\n  variant = \"sidebar\",\n  collapsible = \"offcanvas\",\n  className,\n  children,\n  dir,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  side?: \"left\" | \"right\"\n  variant?: \"sidebar\" | \"floating\" | \"inset\"\n  collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}) {\n  const { isMobile, state, openMobile, setOpenMobile } = useSidebar()\n  /* The mobile sheet is opened by a SidebarTrigger that lives outside it, so\n     Radix has no Dialog.Trigger to hand focus back to and drops it on <body>.\n     The element focused when the sheet opened is kept here and restored. */\n  const returnFocusRef = React.useRef<HTMLElement | null>(null)\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        data-slot=\"sidebar\"\n        className={cn(\n          \"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n\n  if (isMobile) {\n    return (\n      <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n        <SheetContent\n          dir={dir}\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar\"\n          data-mobile=\"true\"\n          className=\"w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden\"\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n            } as React.CSSProperties\n          }\n          side={side}\n          onOpenAutoFocus={() => {\n            const active = document.activeElement\n            returnFocusRef.current = active instanceof HTMLElement ? active : null\n          }}\n          onCloseAutoFocus={(event) => {\n            const target = returnFocusRef.current\n            returnFocusRef.current = null\n            if (target?.isConnected) {\n              event.preventDefault()\n              target.focus()\n            }\n          }}\n        >\n          <SheetHeader className=\"sr-only\">\n            <SheetTitle>Sidebar</SheetTitle>\n            <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n          </SheetHeader>\n          <div className=\"flex h-full w-full flex-col\">{children}</div>\n        </SheetContent>\n      </Sheet>\n    )\n  }\n\n  return (\n    <div\n      className=\"group peer hidden text-sidebar-foreground md:block\"\n      data-state={state}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-variant={variant}\n      data-side={side}\n      data-slot=\"sidebar\"\n    >\n      {/* This is what handles the sidebar gap on desktop. It and the container\n          below must stay on the same clock — they are the same edge drawn\n          twice, and any difference between them shows up as a tear. */}\n      <div\n        data-slot=\"sidebar-gap\"\n        className={cn(\n          \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-spring-glide ease-spring-glide\",\n          \"group-data-[collapsible=offcanvas]:w-0\",\n          \"group-data-[side=right]:rotate-180\",\n          variant === \"floating\" || variant === \"inset\"\n            ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\"\n        )}\n      />\n      <div\n        data-slot=\"sidebar-container\"\n        data-side={side}\n        className={cn(\n          \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-spring-glide ease-spring-glide data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex\",\n          // Adjust the padding for floating and inset variants.\n          variant === \"floating\" || variant === \"inset\"\n            ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar-inner\"\n          className=\"flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border\"\n        >\n          {children}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction SidebarTrigger({\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { toggleSidebar } = useSidebar()\n\n  return (\n    <Button\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon-sm\"\n      className={cn(className)}\n      onClick={(event) => {\n        onClick?.(event)\n        toggleSidebar()\n      }}\n      {...props}\n    >\n      <PanelLeftIcon />\n      <span className=\"sr-only\">Toggle Sidebar</span>\n    </Button>\n  )\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n  const { toggleSidebar } = useSidebar()\n\n  return (\n    <button\n      data-sidebar=\"rail\"\n      data-slot=\"sidebar-rail\"\n      aria-label=\"Toggle Sidebar\"\n      tabIndex={-1}\n      onClick={toggleSidebar}\n      title=\"Toggle Sidebar\"\n      className={cn(\n        /* The rail's hover cue is drawn on its `after` line, which the button's\n           own transition cannot reach — so the line gets its own. */\n        \"absolute inset-y-0 z-20 hidden w-4 transition-all duration-fast ease-out-expo group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] after:transition-colors after:duration-fast after:ease-out-expo hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2\",\n        \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n        \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n        \"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar\",\n        \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n        \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n  return (\n    <main\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        /* The inset variant's left margin changes when the sidebar collapses,\n           so it belongs to that choreography rather than snapping ahead of it.\n           Nothing else here moves, and the layout is already being reflowed by\n           the gap beside it, so this costs nothing extra. */\n        /* min-w-0 is a defect fix, not a style choice. As a flex child the\n           inset defaults to min-width:auto, so any descendant that refuses to\n           shrink — a nowrap table, a wide grid — pushes the inset past the\n           viewport and the whole PAGE scrolls sideways instead of that one\n           region scrolling in its own container. Measured on five screens at\n           900px: three of them overflowed, by 35px, 108px and 140px. min-width:0 has no effect whenever the content\n           already fits, so it changes nothing on a wide viewport. */\n        \"min-w-0 relative flex w-full flex-1 flex-col bg-background transition-[margin] duration-spring-glide ease-spring-glide md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof Input>) {\n  return (\n    <Input\n      data-slot=\"sidebar-input\"\n      data-sidebar=\"input\"\n      className={cn(\"h-8 w-full bg-background shadow-none\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-header\"\n      data-sidebar=\"header\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-footer\"\n      data-sidebar=\"footer\"\n      className={cn(\"flex flex-col gap-2 p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>) {\n  return (\n    <Separator\n      data-slot=\"sidebar-separator\"\n      data-sidebar=\"separator\"\n      className={cn(\"mx-2 w-auto bg-sidebar-border\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-content\"\n      data-sidebar=\"content\"\n      className={cn(\n        \"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group\"\n      data-sidebar=\"group\"\n      className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupLabel({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-label\"\n      data-sidebar=\"group-label\"\n      className={cn(\n        \"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-spring-glide ease-spring-glide group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupAction({\n  className,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-group-action\"\n      data-sidebar=\"group-action\"\n      className={cn(\n        \"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarGroupContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-group-content\"\n      data-sidebar=\"group-content\"\n      className={cn(\"w-full text-sm\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu\"\n      data-sidebar=\"menu\"\n      className={cn(\"flex w-full min-w-0 flex-col gap-0\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-item\"\n      data-sidebar=\"menu-item\"\n      className={cn(\"group/menu-item relative\", className)}\n      {...props}\n    />\n  )\n}\n\n/* Two clocks on one element, which is why the shorthand is spelled out instead\n   of composed from utilities.\n\n   The highlight has to land under the cursor — this is the row a user crosses\n   dozens of times a session, and anything slower than instant reads as lag. The\n   width/height/padding change is a different event entirely: it only happens\n   when the rail collapses, and it has to travel with the container beside it or\n   the buttons snap to icon size while the panel is still moving. Utilities give\n   one element one transition-duration, so the two are declared directly. Every\n   value is still a token. */\nconst sidebarMenuButtonVariants = cva(\n  \"peer/menu-button group/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-[10px] p-2.5 text-left text-body ring-sidebar-ring outline-hidden [transition:background-color_var(--duration-instant)_var(--ease-out-expo),color_var(--duration-instant)_var(--ease-out-expo),width_var(--duration-spring-glide)_var(--ease-spring-glide),height_var(--duration-spring-glide)_var(--ease-spring-glide),padding_var(--duration-spring-glide)_var(--ease-spring-glide)] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-9! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate\",\n  {\n    variants: {\n      variant: {\n        default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n        outline:\n          \"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]\",\n      },\n      size: {\n        default: \"h-8 text-sm\",\n        sm: \"h-7 text-xs\",\n        lg: \"h-12 text-sm group-data-[collapsible=icon]:p-0!\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nfunction SidebarMenuButton({\n  asChild = false,\n  isActive = false,\n  variant = \"default\",\n  size = \"default\",\n  tooltip,\n  className,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean\n  isActive?: boolean\n  tooltip?: string | React.ComponentProps<typeof TooltipContent>\n} & VariantProps<typeof sidebarMenuButtonVariants>) {\n  const Comp = asChild ? Slot.Root : \"button\"\n  const { isMobile, state } = useSidebar()\n\n  const button = (\n    <Comp\n      data-slot=\"sidebar-menu-button\"\n      data-sidebar=\"menu-button\"\n      data-size={size}\n      data-active={isActive}\n      /* `data-active` is a styling hook and nothing more — without this the\n         highlight is invisible to a screen reader. Declared before the spread\n         so a row whose \"active\" means something other than \"this page\" can\n         pass its own value (or `undefined`) and win. */\n      aria-current={isActive ? \"page\" : undefined}\n      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n      {...props}\n    />\n  )\n\n  /* Never shown on mobile, but a mounted tooltip still claims the first Escape\n     inside the sheet, so the sheet took two presses to close. */\n  if (!tooltip || isMobile) {\n    return button\n  }\n\n  if (typeof tooltip === \"string\") {\n    tooltip = {\n      children: tooltip,\n    }\n  }\n\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{button}</TooltipTrigger>\n      <TooltipContent\n        side=\"right\"\n        align=\"center\"\n        hidden={state !== \"collapsed\" || isMobile}\n        {...tooltip}\n      />\n    </Tooltip>\n  )\n}\n\nfunction SidebarMenuAction({\n  className,\n  asChild = false,\n  showOnHover = false,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  asChild?: boolean\n  showOnHover?: boolean\n}) {\n  const Comp = asChild ? Slot.Root : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-action\"\n      data-sidebar=\"menu-action\"\n      className={cn(\n        /* With `showOnHover` this is revealed by hovering the row, so opacity\n           joins the transition — an action that blinks into existence under the\n           cursor reads as a glitch rather than an affordance. */\n        \"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-[opacity,transform] duration-fast ease-out-expo group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0\",\n        showOnHover &&\n          \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuBadge({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sidebar-menu-badge\"\n      data-sidebar=\"menu-badge\"\n      className={cn(\n        \"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* The in-flow trailing slot: counts, keyboard hints, status dots, an external\n   arrow — anything that trails the label rather than floating over it.\n\n   `SidebarMenuBadge` and `SidebarMenuAction` are both absolutely positioned at\n   the same right edge, so a row that wants a count *and* a menu gets them\n   stacked on top of one another. This one participates in the row's flex line\n   instead, which means it composes with both and with each other. It is a\n   child of the button, not a sibling, so it reads the button's `group/…` state\n   rather than the `peer/…` state those two use. */\nfunction SidebarMenuMeta({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"sidebar-menu-meta\"\n      data-sidebar=\"menu-meta\"\n      className={cn(\n        \"ml-auto flex shrink-0 items-center gap-1.5 text-xs font-medium text-sidebar-foreground/70 tabular-nums select-none group-data-[collapsible=icon]:hidden group-hover/menu-button:text-sidebar-accent-foreground group-data-active/menu-button:text-sidebar-accent-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSkeleton({\n  className,\n  showIcon = false,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showIcon?: boolean\n}) {\n  // Random width between 50 to 90%.\n  const [width] = React.useState(() => {\n    return `${Math.floor(Math.random() * 40) + 50}%`\n  })\n\n  return (\n    <div\n      data-slot=\"sidebar-menu-skeleton\"\n      data-sidebar=\"menu-skeleton\"\n      className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n      {...props}\n    >\n      {showIcon && (\n        <Skeleton\n          className=\"size-4 rounded-md\"\n          data-sidebar=\"menu-skeleton-icon\"\n        />\n      )}\n      <Skeleton\n        className=\"h-4 max-w-(--skeleton-width) flex-1\"\n        data-sidebar=\"menu-skeleton-text\"\n        style={\n          {\n            \"--skeleton-width\": width,\n          } as React.CSSProperties\n        }\n      />\n    </div>\n  )\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"sidebar-menu-sub\"\n      data-sidebar=\"menu-sub\"\n      className={cn(\n        \"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSubItem({\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"sidebar-menu-sub-item\"\n      data-sidebar=\"menu-sub-item\"\n      className={cn(\"group/menu-sub-item relative\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction SidebarMenuSubButton({\n  asChild = false,\n  size = \"md\",\n  isActive = false,\n  className,\n  ...props\n}: React.ComponentProps<\"a\"> & {\n  asChild?: boolean\n  size?: \"sm\" | \"md\"\n  isActive?: boolean\n}) {\n  const Comp = asChild ? Slot.Root : \"a\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-menu-sub-button\"\n      data-sidebar=\"menu-sub-button\"\n      data-size={size}\n      data-active={isActive}\n      aria-current={isActive ? \"page\" : undefined}\n      className={cn(\n        /* Same highlight clock as the parent row, minus the collapse travel —\n           sub-items are hidden in icon mode, so they never resize. */\n        \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-colors duration-instant ease-out-expo group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/* ---------------------------------------------------------------------------\n   Nested groups\n\n   The sub-menu primitives above have always existed, but every consumer had to\n   re-derive the same four moving parts by hand: a Collapsible, `asChild` on the\n   row, a chevron wired to the open state, and the sub list. Worse, all of it\n   goes dark in rail mode — `SidebarMenuSub` is `group-data-[collapsible=icon]:\n   hidden`, so a collapsed sidebar simply loses its nested rows with nothing in\n   their place.\n\n   So the packaged version does two things. It renders the four parts once, and\n   it moves the sub-list into a right-side flyout when the sidebar is a rail.\n   One children tree, two presentations — which is why the content component\n   below exists even though the upstream shadcn/ui API does not name one. Without it the\n   caller would have to write the sub list twice and keep the copies in step.\n\n   The flyout portals, and that portal is load-bearing: it escapes the sidebar's\n   `group`, so `group-data-[collapsible=icon]:hidden` stops matching and the\n   rows inside are visible again for free.\n\n   ⚠️ The row itself is wrapped by **both** triggers at all times, never one or\n   the other. Swapping the wrapper on `railMode` looks tidier and is wrong: the\n   two triggers are different component types, so React would unmount the button\n   at the exact frame the rail starts closing, and a freshly mounted element has\n   nothing to transition from. The row would snap to icon size while every row\n   around it glided — the tear the transition comment above exists to prevent.\n   Both wrappers are Slots that merge onto the same button, so the cost of\n   keeping both is one extra `aria-haspopup`, and the popover's own state is\n   held closed whenever the panel is wide. */\ntype SidebarMenuCollapsibleContextProps = {\n  railMode: boolean\n  flyoutOpen: boolean\n  label?: React.ReactNode\n  closeFlyout: () => void\n}\n\nconst SidebarMenuCollapsibleContext =\n  React.createContext<SidebarMenuCollapsibleContextProps | null>(null)\n\nfunction useSidebarMenuCollapsible() {\n  const context = React.useContext(SidebarMenuCollapsibleContext)\n  if (!context) {\n    throw new Error(\n      \"SidebarMenuCollapsible parts must be used within a <SidebarMenuCollapsible />.\"\n    )\n  }\n\n  return context\n}\n\nfunction SidebarMenuCollapsible({\n  className,\n  label,\n  children,\n  ...props\n}: Omit<React.ComponentProps<typeof Collapsible>, \"asChild\"> & {\n  /** Heading for the rail flyout — normally the same text as the trigger. */\n  label?: React.ReactNode\n}) {\n  const { isMobile, state } = useSidebar()\n  const railMode = state === \"collapsed\" && !isMobile\n  const [flyoutOpen, setFlyoutOpen] = React.useState(false)\n\n  /* Gating the setter rather than only the render is what stops a click made\n     while the panel was wide from springing a flyout the instant it narrows. */\n  const setFlyout = React.useCallback(\n    (next: boolean) => setFlyoutOpen(next && railMode),\n    [railMode]\n  )\n\n  /* Re-expanding the sidebar takes the flyout with it — otherwise it hangs\n     beside a panel that has already put the same rows back inline. */\n  React.useEffect(() => {\n    if (!railMode) {\n      setFlyoutOpen(false)\n    }\n  }, [railMode])\n\n  const contextValue = React.useMemo<SidebarMenuCollapsibleContextProps>(\n    () => ({\n      railMode,\n      flyoutOpen,\n      label,\n      closeFlyout: () => setFlyoutOpen(false),\n    }),\n    [railMode, flyoutOpen, label]\n  )\n\n  return (\n    <SidebarMenuCollapsibleContext.Provider value={contextValue}>\n      <Collapsible asChild {...props}>\n        {/* Restated because the Slot merge lets Collapsible's own `data-slot`\n            through onto this element otherwise. `data-sidebar` survives on its\n            own — that is the one the stylesheet selects on. */}\n        <SidebarMenuItem\n          data-slot=\"sidebar-menu-item\"\n          className={cn(\"group/collapsible\", className)}\n        >\n          {/* Popover.Root renders no element of its own, so this nests inside\n              the list item without adding a wrapper to the layout. */}\n          <Popover open={railMode && flyoutOpen} onOpenChange={setFlyout}>\n            {children}\n          </Popover>\n        </SidebarMenuItem>\n      </Collapsible>\n    </SidebarMenuCollapsibleContext.Provider>\n  )\n}\n\nfunction SidebarMenuCollapsibleTrigger({\n  children,\n  ...props\n}: React.ComponentProps<typeof SidebarMenuButton>) {\n  const { railMode, flyoutOpen } = useSidebarMenuCollapsible()\n\n  /* Collapsible outside, popover inside: whatever the wrappers merge in, props\n     declared on this element win, which is the seam the rail override uses.\n\n     Inline, the row correctly reports the collapsible it expands. On the rail\n     the collapsible is not rendered at all — the flyout stands in for it — so\n     the row has to report *that* instead, or it announces an expanded region\n     that is neither on screen nor in the document. */\n  const railAria = railMode\n    ? { \"aria-expanded\": flyoutOpen, \"aria-controls\": undefined }\n    : null\n\n  return (\n    <CollapsibleTrigger asChild>\n      <PopoverTrigger asChild>\n        <SidebarMenuButton\n          data-slot=\"sidebar-menu-button\"\n          aria-haspopup={railMode ? \"dialog\" : undefined}\n          {...props}\n          {...railAria}\n        >\n          {children}\n          {/* A quarter turn, deliberately shorter than the panel it labels: the\n              chevron is a label for a state change, not the change itself. */}\n          <ChevronRightIcon\n            aria-hidden=\"true\"\n            className=\"ml-auto shrink-0 text-sidebar-foreground/60 transition-transform duration-base ease-out-expo group-data-[collapsible=icon]:hidden group-data-open/collapsible:rotate-90\"\n          />\n        </SidebarMenuButton>\n      </PopoverTrigger>\n    </CollapsibleTrigger>\n  )\n}\n\nfunction SidebarMenuCollapsibleContent({\n  children,\n  ...props\n}: React.ComponentProps<typeof CollapsibleContent>) {\n  const { railMode, label, closeFlyout } = useSidebarMenuCollapsible()\n\n  if (railMode) {\n    return (\n      <SidebarMenuSubFlyout\n        label={label}\n        /* Picking a destination is the end of the flyout's job. Radix only\n           closes on interaction *outside*, and a rail flyout left standing over\n           the page after a navigation reads as a stuck menu. */\n        onClick={(event) => {\n          const target = event.target\n          if (\n            target instanceof Element &&\n            target.closest(\"[data-sidebar=menu-sub-button]\")\n          ) {\n            closeFlyout()\n          }\n        }}\n      >\n        {children}\n      </SidebarMenuSubFlyout>\n    )\n  }\n\n  return <CollapsibleContent {...props}>{children}</CollapsibleContent>\n}\n\nfunction SidebarMenuSubFlyout({\n  className,\n  label,\n  side = \"right\",\n  align = \"start\",\n  sideOffset = 8,\n  children,\n  ...props\n}: React.ComponentProps<typeof PopoverContent> & {\n  label?: React.ReactNode\n}) {\n  return (\n    <PopoverContent\n      data-slot=\"sidebar-menu-sub-flyout\"\n      data-sidebar=\"menu-sub-flyout\"\n      side={side}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"w-56 gap-1 rounded-xl bg-sidebar p-1.5 text-sidebar-foreground\",\n        /* The sub list drops its indent rail in here. The flyout *is* the\n           indent — a guide line inside it would draw the same relationship a\n           second time, offset from nothing. */\n        \"[&_[data-sidebar=menu-sub]]:mx-0 [&_[data-sidebar=menu-sub]]:translate-x-0 [&_[data-sidebar=menu-sub]]:border-l-0 [&_[data-sidebar=menu-sub]]:p-0\",\n        \"[&_[data-sidebar=menu-sub-button]]:translate-x-0\",\n        /* Panel spring in, short tween out — both inherited from PopoverContent.\n           The blur is the one addition: an enter-only materialise cue, resolving\n           to 0, on a surface that appears out of a 3rem rail with no context. */\n        \"data-open:blur-in-3\",\n        className\n      )}\n      {...props}\n    >\n      {label ? (\n        <div\n          data-slot=\"sidebar-menu-sub-flyout-label\"\n          className=\"px-2 pt-0.5 pb-1 text-xs font-medium text-sidebar-foreground/70\"\n        >\n          {label}\n        </div>\n      ) : null}\n      {children}\n    </PopoverContent>\n  )\n}\n\n/* The account row the `lg` size was always for: avatar, name, one line of\n   secondary text, and a trailing affordance. Packaged rather than left as a\n   composition because getting the two-line grid to truncate correctly inside a\n   row that also has to survive collapsing to 36px is the fiddly part, and every\n   app shell needs exactly one of these. */\nfunction SidebarUser({\n  className,\n  name,\n  secondary,\n  avatar,\n  action,\n  asChild = false,\n  children,\n  ...props\n}: React.ComponentProps<\"button\"> & {\n  name: React.ReactNode\n  secondary?: React.ReactNode\n  avatar?: React.ReactNode\n  action?: React.ReactNode\n  asChild?: boolean\n}) {\n  const Comp = asChild ? Slot.Root : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"sidebar-user\"\n      data-sidebar=\"user\"\n      className={cn(\n        sidebarMenuButtonVariants({ size: \"lg\" }),\n        \"gap-2 group-data-[collapsible=icon]:justify-center\",\n        className\n      )}\n      {...props}\n    >\n      {/* Slottable marks the caller's element as the one to render *as*; the\n          siblings below become its children instead of being thrown away. That\n          path re-parents them as an array, which is why they carry keys. */}\n      {asChild ? <Slot.Slottable>{children}</Slot.Slottable> : null}\n      {avatar ? (\n        <span\n          key=\"avatar\"\n          data-slot=\"sidebar-user-avatar\"\n          className=\"flex shrink-0 items-center justify-center\"\n        >\n          {avatar}\n        </span>\n      ) : null}\n      <span\n        key=\"text\"\n        className=\"grid min-w-0 flex-1 text-left leading-tight group-data-[collapsible=icon]:hidden\"\n      >\n        <span data-slot=\"sidebar-user-name\" className=\"truncate font-medium\">\n          {name}\n        </span>\n        {secondary ? (\n          <span\n            data-slot=\"sidebar-user-secondary\"\n            className=\"truncate text-xs font-normal text-sidebar-foreground/70\"\n          >\n            {secondary}\n          </span>\n        ) : null}\n      </span>\n      {action ? (\n        <span\n          key=\"action\"\n          data-slot=\"sidebar-user-action\"\n          className=\"flex shrink-0 items-center text-sidebar-foreground/60 group-data-[collapsible=icon]:hidden\"\n        >\n          {action}\n        </span>\n      ) : null}\n    </Comp>\n  )\n}\n\nexport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupAction,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarInput,\n  SidebarInset,\n  SidebarMenu,\n  SidebarMenuAction,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuCollapsible,\n  SidebarMenuCollapsibleContent,\n  SidebarMenuCollapsibleTrigger,\n  SidebarMenuItem,\n  SidebarMenuMeta,\n  SidebarMenuSkeleton,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubFlyout,\n  SidebarMenuSubItem,\n  SidebarProvider,\n  SidebarRail,\n  SidebarSeparator,\n  SidebarTrigger,\n  SidebarUser,\n  useSidebar,\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}