{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sidebar","title":"Sidebar","description":"Application navigation sidebar with desktop collapse and a mobile sheet.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/input","@siteplane/scroll-area","@siteplane/separator","@siteplane/sheet","@siteplane/skeleton","@siteplane/tooltip"],"files":[{"path":"packages/ui/src/components/sidebar/sidebar.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { PanelLeftIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useMediaQuery } from \"../../system/hooks/use-media-query\";\nimport { focusRingRecipe } from \"../../system/recipes/focus-ring.recipe\";\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { Button, type ButtonBaseProps } from \"../button\";\nimport { KitButton } from \"../button/button-internal\";\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { Input } from \"../input\";\nimport { KitInput } from \"../input/input-internal\";\nimport { ScrollArea } from \"../scroll-area\";\nimport { Separator } from \"../separator\";\nimport { Sheet, SheetHeader, SheetTitle } from \"../sheet\";\nimport { KitSheetPopup } from \"../sheet/sheet-internal\";\nimport type { KitSidebarProviderProps } from \"./sidebar-internal\";\nimport { Skeleton } from \"../skeleton\";\nimport {\n  Tooltip,\n  TooltipPopup,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"../tooltip\";\n\nconst SIDEBAR_COOKIE_NAME: string = \"sidebar_state\";\nconst SIDEBAR_COOKIE_MAX_AGE: number = 60 * 60 * 24 * 7;\n// The desktop widths are the stylesheet's (design-rework.css, \"Sidebar\").\nconst SIDEBAR_WIDTH_MOBILE: string = \"18rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT: string = \"b\";\n\nexport const sidebarMenuPopupWidthClassName = \"w-56\";\n\ntype SidebarMenuButtonSize = SiteplaneControlSize;\ntype SidebarMenuButtonVariant = \"default\" | \"outline\" | \"workspace\";\n\nexport type SidebarLayout = \"inset\" | \"push\";\n\nexport type SidebarContextProps = {\n  layout?: SidebarLayout | undefined;\n  state: \"expanded\" | \"collapsed\";\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  openMobile: boolean;\n  setOpenMobile: (open: boolean) => void;\n  isMobile: boolean;\n  mobileBreakpoint: \"md\" | \"lg\";\n  toggleSidebar: () => void;\n};\n\nexport const SidebarContext: React.Context<SidebarContextProps | null> =\n  React.createContext<SidebarContextProps | null>(null);\n\nexport function useSidebar(): SidebarContextProps {\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\n// A layout reserves the sidebar's leading 8px gutter on the container itself\n// (the default variant only; floating and inset already pad the container).\n// Header, groups and footer then add only their trailing inset, so a layout\n// sidebar insets rows by the same 8px on both sides and every part keeps its\n// block padding.\nconst SidebarGutterContext: React.Context<boolean> = React.createContext(false);\n\n// The id that SidebarTrigger and SidebarRail name in aria-controls: the\n// provider's own, or the id the consumer gives Sidebar.\ntype SidebarControls = {\n  id: string;\n  setId: (id: string | undefined) => void;\n};\n\nconst SidebarControlsContext: React.Context<SidebarControls | null> =\n  React.createContext<SidebarControls | null>(null);\n\ntype SidebarCollapsible = \"offcanvas\" | \"icon\" | \"none\";\n\n// Sidebar tells its parts how it collapses, so a menu item knows when it sits\n// on the collapsed icon rail.\nconst SidebarCollapsibleContext: React.Context<SidebarCollapsible | null> =\n  React.createContext<SidebarCollapsible | null>(null);\n\n// A menu item with an expand trigger tells its row to step aside while the\n// trigger stands in for it on the collapsed icon rail.\nconst SidebarMenuItemContext: React.Context<boolean> =\n  React.createContext(false);\n\n// Ctrl/Cmd+B stays with a field that edits text: there it is Bold, or a caret\n// move in macOS text fields.\nconst nonTextInputTypes: ReadonlySet<string> = new Set([\n  \"button\",\n  \"checkbox\",\n  \"color\",\n  \"file\",\n  \"hidden\",\n  \"image\",\n  \"radio\",\n  \"range\",\n  \"reset\",\n  \"submit\",\n]);\n\nfunction isEditableTarget(target: EventTarget | null): boolean {\n  if (typeof HTMLElement === \"undefined\" || !(target instanceof HTMLElement)) {\n    return false;\n  }\n  if (target.isContentEditable) return true;\n  if (\n    target instanceof HTMLTextAreaElement ||\n    target instanceof HTMLSelectElement\n  ) {\n    return true;\n  }\n  return (\n    target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type)\n  );\n}\n\nfunction isCurrent(ariaCurrent: React.AriaAttributes[\"aria-current\"]): boolean {\n  return (\n    ariaCurrent !== undefined &&\n    ariaCurrent !== false &&\n    ariaCurrent !== \"false\"\n  );\n}\n\nfunction getSidebarLengthInPixels(\n  element: HTMLElement,\n  name: string,\n  fallback: number,\n): number {\n  const value = getComputedStyle(element).getPropertyValue(name).trim();\n  const parsedValue = Number.parseFloat(value);\n\n  if (!Number.isFinite(parsedValue)) {\n    return fallback;\n  }\n\n  if (value.endsWith(\"rem\")) {\n    const rootFontSize = Number.parseFloat(\n      getComputedStyle(document.documentElement).fontSize,\n    );\n\n    return parsedValue * (Number.isFinite(rootFontSize) ? rootFontSize : 16);\n  }\n\n  return parsedValue;\n}\n\n// Persistence is best effort: the Cookie Store API is missing in some browsers\n// and in insecure contexts, and a blocked write must never reject.\nfunction persistSidebarOpenState(open: boolean, name: string): void {\n  const value = String(open);\n  const store = (globalThis as { cookieStore?: CookieStore }).cookieStore;\n\n  try {\n    if (store && typeof store.set === \"function\") {\n      store\n        .set({\n          expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,\n          name,\n          path: \"/\",\n          value,\n        })\n        .catch(() => undefined);\n      return;\n    }\n\n    if (typeof document !== \"undefined\") {\n      document.cookie = `${name}=${value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n    }\n  } catch {\n    // Cookies can be unavailable, for example in a sandboxed document.\n  }\n}\n\ntype SidebarProviderProps = React.ComponentProps<\"div\"> & {\n  layout?: SidebarLayout;\n  defaultOpen?: boolean;\n  mobileBreakpoint?: \"md\" | \"lg\";\n  keyboardShortcut?: boolean;\n  /**\n   * The cookie that keeps the desktop state: its name, or `false` for no\n   * cookie. The provider writes it on every real change and never reads it;\n   * pass its value to `defaultOpen` on the server. Default `sidebar_state`.\n   */\n  stateCookie?: string | false;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nexport function SidebarProvider(\n  providerProps: SidebarProviderProps,\n): React.ReactElement {\n  // Header Navigation passes the internal forceMobile through\n  // KitSidebarProvider (sidebar-internal.ts), the same function.\n  const {\n    layout,\n    defaultOpen = true,\n    mobileBreakpoint = \"md\",\n    keyboardShortcut = true,\n    stateCookie = SIDEBAR_COOKIE_NAME,\n    open: openProp,\n    onOpenChange: setOpenProp,\n    className,\n    style,\n    children,\n    forceMobile = false,\n    ...props\n  } = providerProps as KitSidebarProviderProps;\n  const viewportMobile = useMediaQuery(`max-${mobileBreakpoint}`);\n  const isMobile = forceMobile || viewportMobile;\n  const [openMobile, setOpenMobile] = React.useState(false);\n  const generatedId = React.useId();\n  const [sidebarId, setSidebarId] = React.useState<string>();\n  const controls = React.useMemo<SidebarControls>(\n    () => ({ id: sidebarId ?? generatedId, setId: setSidebarId }),\n    [generatedId, sidebarId],\n  );\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  // An empty name is no cookie either.\n  const cookieName = stateCookie || null;\n  const setOpen = React.useCallback(\n    (value: boolean | ((value: boolean) => boolean)) => {\n      const openState = typeof value === \"function\" ? value(open) : value;\n      if (openProp === undefined) {\n        _setOpen(openState);\n        // The cookie keeps the sidebar state and records real changes only.\n        if (openState !== open && cookieName)\n          persistSidebarOpenState(openState, cookieName);\n      }\n      setOpenProp?.(openState);\n    },\n    [setOpenProp, openProp, open, cookieName],\n  );\n\n  // A controlled state reaches the cookie once its parent has applied it, so\n  // a change the parent rejects is never written.\n  const persistedOpenProp = React.useRef(openProp);\n  React.useEffect(() => {\n    if (openProp === persistedOpenProp.current) return;\n    persistedOpenProp.current = openProp;\n    if (openProp !== undefined && cookieName)\n      persistSidebarOpenState(openProp, cookieName);\n  }, [openProp, cookieName]);\n\n  // Helper to toggle the sidebar.\n  const toggleSidebar = React.useCallback(() => {\n    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);\n  }, [isMobile, setOpen]);\n\n  // Adds a keyboard shortcut to toggle the sidebar.\n  React.useEffect(() => {\n    if (!keyboardShortcut) return;\n    const handleKeyDown = (event: KeyboardEvent): void => {\n      if (\n        event.key !== SIDEBAR_KEYBOARD_SHORTCUT ||\n        !(event.metaKey || event.ctrlKey) ||\n        // Another handler took the keys, an input method is composing, or a\n        // text field or editor has focus: the keys are theirs.\n        event.defaultPrevented ||\n        event.isComposing ||\n        isEditableTarget(event.target)\n      ) {\n        return;\n      }\n      event.preventDefault();\n      toggleSidebar();\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [toggleSidebar, keyboardShortcut]);\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      layout,\n      isMobile,\n      mobileBreakpoint,\n      open,\n      openMobile,\n      setOpen,\n      setOpenMobile,\n      state,\n      toggleSidebar,\n    }),\n    [\n      layout,\n      state,\n      open,\n      setOpen,\n      isMobile,\n      mobileBreakpoint,\n      openMobile,\n      toggleSidebar,\n    ],\n  );\n\n  return (\n    <SidebarContext.Provider value={contextValue}>\n      <SidebarControlsContext.Provider value={controls}>\n        <div\n          className={cn(\n            slotClass(\"sidebar-wrapper\", props),\n            \"group/sidebar-wrapper flex min-h-svh w-full bg-[var(--sp-rw-bg-base)] text-[var(--sp-rw-fg-primary)]\",\n            className,\n          )}\n          data-layout={layout}\n          data-slot=\"sidebar-wrapper\"\n          data-state={state}\n          // The widths are the stylesheet's (design-rework.css, \"Sidebar\"),\n          // so a policy that blocks style attributes keeps them; an app's style\n          // still overrides them.\n          style={style}\n          {...props}\n        >\n          {children}\n        </div>\n      </SidebarControlsContext.Provider>\n    </SidebarContext.Provider>\n  );\n}\n\nexport function Sidebar({\n  side = \"left\",\n  variant = \"sidebar\",\n  mobileVariant = \"default\",\n  collapsible = \"offcanvas\",\n  id: idProp,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  \"aria-describedby\": ariaDescribedBy,\n  className,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  side?: \"left\" | \"right\";\n  variant?: \"sidebar\" | \"floating\" | \"inset\";\n  mobileVariant?: \"default\" | \"fullscreen\";\n  collapsible?: SidebarCollapsible;\n}): React.ReactElement {\n  const {\n    layout,\n    isMobile,\n    mobileBreakpoint,\n    state,\n    openMobile,\n    setOpenMobile,\n  } = useSidebar();\n  const controls = React.useContext(SidebarControlsContext);\n  const setControlsId = controls?.setId;\n  // A consumer's id names the sidebar for the triggers' aria-controls.\n  React.useEffect(() => {\n    if (idProp === undefined || !setControlsId) return;\n    setControlsId(idProp);\n    return () => setControlsId(undefined);\n  }, [idProp, setControlsId]);\n  const id = idProp ?? controls?.id;\n  // The landmark (desktop) or the dialog (mobile) carries the name. Only the\n  // attributes the consumer sets are passed on: an undefined aria-labelledby\n  // would replace the one the sheet's title gives its dialog.\n  const labelProps = {\n    ...(ariaDescribedBy === undefined\n      ? null\n      : { \"aria-describedby\": ariaDescribedBy }),\n    ...(ariaLabelledBy === undefined\n      ? null\n      : { \"aria-labelledby\": ariaLabelledBy }),\n    id,\n  };\n  // The sidebar sits flat on the page background in every layout: no frame,\n  // border or shadow of its own. The main surface carries the edge.\n  const layoutClassName = cn(\n    layout && \"ps-2\",\n    layout === \"push\" && \"z-20 bg-[var(--sp-rw-bg-base)]\",\n  );\n  // The sidebar's own tooltips open at once; tooltips elsewhere in the app keep\n  // their provider's delay.\n  const content = (\n    <TooltipProvider delay={0}>\n      <SidebarCollapsibleContext.Provider value={collapsible}>\n        {children}\n      </SidebarCollapsibleContext.Provider>\n    </TooltipProvider>\n  );\n\n  if (collapsible === \"none\") {\n    return (\n      <div\n        aria-label={ariaLabel}\n        className={cn(\n          slotClass(\"sidebar\", props),\n          \"flex h-full w-(--sidebar-width) flex-col bg-[var(--sp-rw-bg-base)] text-[var(--sp-rw-fg-primary)]\",\n          layoutClassName,\n          className,\n        )}\n        data-slot=\"sidebar\"\n        style={style}\n        {...labelProps}\n        {...props}\n      >\n        <SidebarGutterContext.Provider value={Boolean(layout)}>\n          {content}\n        </SidebarGutterContext.Provider>\n      </div>\n    );\n  }\n\n  if (isMobile) {\n    // Every prop reaches the sheet popup, the element the sidebar is on a\n    // small screen; aria-label becomes the dialog's title.\n    return (\n      <Sheet onOpenChange={setOpenMobile} open={openMobile}>\n        <KitSheetPopup\n          className={className}\n          data-mobile=\"true\"\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar\"\n          profile=\"sidebar\"\n          showCloseButton={false}\n          side={mobileVariant === \"fullscreen\" ? \"top\" : side}\n          variant={mobileVariant}\n          style={\n            {\n              \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n              ...style,\n            } as React.CSSProperties\n          }\n          {...labelProps}\n          {...props}\n        >\n          <SheetHeader className=\"sr-only\">\n            <SheetTitle>{ariaLabel ?? \"Sidebar\"}</SheetTitle>\n          </SheetHeader>\n          <SidebarGutterContext.Provider value={Boolean(layout)}>\n            <div\n              className={cn(\"flex h-full w-full flex-col\", layout && \"ps-2\")}\n            >\n              {content}\n            </div>\n          </SidebarGutterContext.Provider>\n        </KitSheetPopup>\n      </Sheet>\n    );\n  }\n\n  // The desktop sidebar is a complementary landmark, so its navigation, account\n  // controls and header content stay within a landmark region. The landmark\n  // takes the id and the name; className, style and the other props stay on\n  // the sidebar container they always styled.\n  return (\n    <aside\n      aria-label={ariaLabel}\n      className={cn(\n        \"sp-sidebar\",\n        \"group peer hidden text-[var(--sp-rw-fg-primary)]\",\n        mobileBreakpoint === \"lg\" ? \"lg:block\" : \"md:block\",\n      )}\n      data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n      data-side={side}\n      data-slot=\"sidebar\"\n      data-state={state}\n      data-variant={variant}\n      {...labelProps}\n    >\n      {/* This is what handles the sidebar gap on desktop */}\n      <div\n        className={cn(\n          \"sp-sidebar-gap\",\n          \"relative w-[var(--sidebar-gap-width,var(--sidebar-width))] bg-transparent transition-[width] duration-(--motion-duration-base) ease-(--motion-ease-linear)\",\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        data-slot=\"sidebar-gap\"\n      />\n      {/* A collapsed offcanvas sidebar leaves the tab order and the\n          accessibility tree once it has slid out (visibility switches at the\n          end of the slide and at its start when it opens). The rail stays\n          visible to open it again. The side is the inline start or end, so\n          a right-to-left page puts a left sidebar on the right, where its gap\n          is. */}\n      <div\n        className={cn(\n          slotClass(\"sidebar-container\", props),\n          \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width,visibility] duration-(--motion-duration-base) ease-(--motion-ease-linear)\",\n          mobileBreakpoint === \"lg\" ? \"lg:flex\" : \"md:flex\",\n          side === \"left\"\n            ? \"start-0 group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)*-1)]\"\n            : \"end-0 group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)*-1)]\",\n          \"group-data-[collapsible=offcanvas]:invisible\",\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)))]\"\n            : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\",\n          layoutClassName,\n          className,\n        )}\n        data-layout={layout}\n        data-slot=\"sidebar-container\"\n        style={style}\n        {...props}\n      >\n        <div\n          className=\"sp-sidebar-inner flex h-full w-full flex-col bg-transparent group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:bg-[var(--sp-rw-bg-primary)] group-data-[variant=floating]:shadow-[var(--sp-rw-surface-xs-shadow)]\"\n          data-sidebar=\"sidebar\"\n          data-slot=\"sidebar-inner\"\n        >\n          <SidebarGutterContext.Provider\n            value={Boolean(layout) && variant === \"sidebar\"}\n          >\n            {content}\n          </SidebarGutterContext.Provider>\n        </div>\n      </div>\n    </aside>\n  );\n}\n\n// The state a trigger announces: the desktop expansion, or on a small screen\n// whether the sheet is open. aria-controls names the sidebar while it exists.\nfunction useSidebarTriggerState(): {\n  \"aria-controls\": string | undefined;\n  \"aria-expanded\": boolean;\n} {\n  const { isMobile, open, openMobile } = useSidebar();\n  const controls = React.useContext(SidebarControlsContext);\n  const expanded = isMobile ? openMobile : open;\n\n  return {\n    \"aria-controls\": isMobile && !openMobile ? undefined : controls?.id,\n    \"aria-expanded\": expanded,\n  };\n}\n\nexport function SidebarTrigger({\n  children,\n  className,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof Button>): React.ReactElement {\n  const { toggleSidebar } = useSidebar();\n  const triggerState = useSidebarTriggerState();\n\n  return (\n    <KitButton\n      {...triggerState}\n      className={className}\n      data-sidebar=\"trigger\"\n      data-slot=\"sidebar-trigger\"\n      onClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n        onClick?.(event);\n        // The mobile sheet has no trigger of its own, so on close Base UI\n        // returns focus to the element that had it before the sheet opened.\n        // Safari focuses no button it presses; the trigger takes focus itself,\n        // without the keyboard ring, so closing returns focus to it there too.\n        const trigger = event.currentTarget;\n        if (trigger.ownerDocument.activeElement !== trigger)\n          trigger.focus({\n            focusVisible: false,\n            preventScroll: true,\n          } as FocusOptions);\n        toggleSidebar();\n      }}\n      profile=\"compact-icon-action\"\n      shape=\"square\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? (\n        <>\n          {/* The panel glyph shows the sidebar at the inline start, so it\n              mirrors in a right-to-left page. */}\n          <PanelLeftIcon className=\"rtl:-scale-x-100\" />\n          <span className=\"sr-only\">Toggle sidebar</span>\n        </>\n      )}\n    </KitButton>\n  );\n}\n\nexport function SidebarRail({\n  className,\n  resizable = false,\n  onClick,\n  onPointerDown,\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  resizable?: boolean;\n}): React.ReactElement {\n  const { state, toggleSidebar } = useSidebar();\n  const triggerState = useSidebarTriggerState();\n  const didDragRef = React.useRef(false);\n  // Ends a resize drag: restores the page's cursor and text selection.\n  const endDragRef = React.useRef<(() => void) | null>(null);\n\n  React.useEffect(() => () => endDragRef.current?.(), []);\n\n  function handlePointerDown(\n    event: React.PointerEvent<HTMLButtonElement>,\n  ): void {\n    onPointerDown?.(event);\n    // A new press starts fresh: only a drag in this press may swallow its\n    // click.\n    didDragRef.current = false;\n    if (event.button !== 0 || !resizable || state !== \"expanded\") {\n      return;\n    }\n\n    const sidebar = event.currentTarget.closest<HTMLElement>(\n      '[data-slot=\"sidebar\"]',\n    );\n    const sidebarContainer = sidebar?.querySelector<HTMLElement>(\n      '[data-slot=\"sidebar-container\"]',\n    );\n    const sidebarWrapper = event.currentTarget.closest<HTMLElement>(\n      '[data-slot=\"sidebar-wrapper\"]',\n    );\n\n    if (!sidebar || !sidebarContainer || !sidebarWrapper) {\n      return;\n    }\n\n    const wrapper = sidebarWrapper;\n    // The side is the inline start or end: in a right-to-left page a left\n    // sidebar sits on the right, so its edge moves the other way.\n    const rightToLeft = getComputedStyle(sidebar).direction === \"rtl\";\n    const onPhysicalRight =\n      (sidebar.dataset.side ?? \"left\") === \"right\" ? !rightToLeft : rightToLeft;\n    const startX = event.clientX;\n    const startWidth = sidebarContainer.getBoundingClientRect().width;\n    const minWidth = getSidebarLengthInPixels(\n      wrapper,\n      \"--sidebar-width-min\",\n      192,\n    );\n    const maxWidth = getSidebarLengthInPixels(\n      wrapper,\n      \"--sidebar-width-max\",\n      384,\n    );\n    // A drag that never saw its release ends before the next one starts.\n    endDragRef.current?.();\n    const previousCursor = document.body.style.cursor;\n    const previousUserSelect = document.body.style.userSelect;\n\n    didDragRef.current = false;\n    document.body.style.cursor = \"col-resize\";\n    document.body.style.userSelect = \"none\";\n\n    function handlePointerMove(moveEvent: PointerEvent): void {\n      const delta = onPhysicalRight\n        ? startX - moveEvent.clientX\n        : moveEvent.clientX - startX;\n\n      if (Math.abs(delta) > 3) {\n        didDragRef.current = true;\n      }\n\n      const nextWidth = Math.min(\n        maxWidth,\n        Math.max(minWidth, startWidth + delta),\n      );\n\n      wrapper.style.setProperty(\n        \"--sidebar-width\",\n        `${Math.round(nextWidth)}px`,\n      );\n    }\n\n    // A release, a cancelled touch drag or an unmount ends the drag. Only a\n    // release is followed by the click that the drag must not turn into a\n    // toggle.\n    function endDrag(endEvent?: PointerEvent): void {\n      document.body.style.cursor = previousCursor;\n      document.body.style.userSelect = previousUserSelect;\n      window.removeEventListener(\"pointermove\", handlePointerMove);\n      window.removeEventListener(\"pointerup\", endDrag);\n      window.removeEventListener(\"pointercancel\", endDrag);\n      endDragRef.current = null;\n      if (endEvent?.type !== \"pointerup\") didDragRef.current = false;\n    }\n\n    endDragRef.current = endDrag;\n    window.addEventListener(\"pointermove\", handlePointerMove);\n    window.addEventListener(\"pointerup\", endDrag);\n    window.addEventListener(\"pointercancel\", endDrag);\n  }\n\n  // Supplied handlers run first; the rail still toggles and resizes.\n  return (\n    <KitButton\n      {...triggerState}\n      aria-label=\"Toggle sidebar\"\n      // An 8 px strip along the whole edge, on its outer side: it leaves the\n      // sidebar's own scrollbar free and reaches as little as it can into the\n      // content beside it. A collapsed offcanvas sidebar keeps it at the\n      // screen edge. The edge is the sidebar's inline end, or its start for a\n      // right sidebar, in either direction.\n      className={cn(\n        \"absolute inset-y-0 z-20 hidden w-2 group-data-[side=left]:start-full group-data-[side=right]:end-full sm:flex\",\n        \"group-data-[collapsible=offcanvas]:visible\",\n        className,\n      )}\n      data-resizable={resizable ? \"\" : undefined}\n      data-sidebar=\"rail\"\n      data-slot=\"sidebar-rail\"\n      onClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n        onClick?.(event);\n        if (didDragRef.current) {\n          event.preventDefault();\n          didDragRef.current = false;\n          return;\n        }\n\n        toggleSidebar();\n      }}\n      onPointerDown={handlePointerDown}\n      profile=\"sidebar-rail\"\n      tabIndex={-1}\n      title={resizable ? \"Resize or toggle sidebar\" : undefined}\n      variant=\"text\"\n      {...props}\n    />\n  );\n}\n\n// The content's desktop surface starts where the desktop sidebar does, at the\n// provider's mobile breakpoint, so it never frames itself beside a sidebar that\n// is still a sheet. From there SidebarInset is the primary surface beside the\n// sidebar on the page canvas, with a layout or without one, so the two never\n// share one grey (rulebook B1). Sidebar `variant=\"inset\"` and `layout=\"inset\"`\n// frame that surface. Tailwind needs every class spelled out, hence one set\n// per breakpoint.\nconst sidebarInsetClassNames = {\n  lg: {\n    inset:\n      \"lg:m-2 lg:ms-0 lg:rounded-2xl lg:shadow-[var(--sp-rw-surface-sm-shadow)]\",\n    push: \"lg:peer-data-[state=expanded]:[--sidebar-content-offset:calc(var(--sidebar-width)-var(--sidebar-width-icon))]\",\n    surface: \"lg:bg-[var(--sp-rw-bg-primary)]\",\n    variantInset:\n      \"lg:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2 lg:peer-data-[variant=inset]:m-2 lg:peer-data-[variant=inset]:ms-0 lg:peer-data-[variant=inset]:rounded-2xl lg:peer-data-[variant=inset]:shadow-[var(--sp-rw-surface-sm-shadow)]\",\n  },\n  md: {\n    inset:\n      \"md:m-2 md:ms-0 md:rounded-2xl md:shadow-[var(--sp-rw-surface-sm-shadow)]\",\n    push: \"md:peer-data-[state=expanded]:[--sidebar-content-offset:calc(var(--sidebar-width)-var(--sidebar-width-icon))]\",\n    surface: \"md:bg-[var(--sp-rw-bg-primary)]\",\n    variantInset:\n      \"md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-2xl md:peer-data-[variant=inset]:shadow-[var(--sp-rw-surface-sm-shadow)]\",\n  },\n} as const;\n\n// In the push layout the expanded Sidebar lies over the content surface, which\n// starts beside the icon rail. SidebarInset moves its content clear of it by\n// --sidebar-content-offset, the expanded width beyond the rail (0 when the\n// sidebar is collapsed or a sheet), and moves it with the Sidebar's own pace\n// (design-rework.css, [data-slot=\"sidebar-inset\"][data-layout=\"push\"]). Its\n// children keep their own padding and need no offset or motion code.\nexport function SidebarInset({\n  className,\n  ...props\n}: React.ComponentProps<\"main\">): React.ReactElement {\n  const { layout, mobileBreakpoint } = useSidebar();\n  const desktop = sidebarInsetClassNames[mobileBreakpoint];\n  return (\n    <main\n      className={cn(\n        slotClass(\"sidebar-inset\", props),\n        \"relative flex w-full flex-1 flex-col bg-background\",\n        desktop.surface,\n        desktop.variantInset,\n        layout &&\n          \"min-w-0 overflow-hidden bg-transparent [--sidebar-content-offset:0px]\",\n        layout === \"push\" && desktop.push,\n        layout === \"inset\" && desktop.inset,\n        className,\n      )}\n      data-layout={layout}\n      data-slot=\"sidebar-inset\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof Input>): React.ReactElement {\n  return (\n    <KitInput\n      className={className}\n      data-sidebar=\"input\"\n      data-slot=\"sidebar-input\"\n      profile=\"sidebar\"\n      {...props}\n    />\n  );\n}\n\n// Header and footer are bands with the same 8px above and below (S2). On the\n// collapsed icon rail their items keep the inset they have in the expanded\n// sidebar, as the rows do, so nothing moves while the width animates: the rail\n// is a clipped view with fixed insets, as in shadcn's sidebar. A 36px item,\n// such as the brand's square or a row, stands on the rail's centre axis. The\n// items never grow wider than the rail.\nconst sidebarBandRailClassName = \"group-data-[collapsible=icon]:*:max-w-full\";\n\nexport function SidebarHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  const leadingGutter = React.useContext(SidebarGutterContext);\n\n  return (\n    <div\n      className={cn(\n        slotClass(\"sidebar-header\", props),\n        \"flex flex-col gap-2 p-2\",\n        sidebarBandRailClassName,\n        leadingGutter && \"ps-0\",\n        className,\n      )}\n      data-sidebar=\"header\"\n      data-slot=\"sidebar-header\"\n      {...props}\n    />\n  );\n}\n\n/**\n * The application's logo at the top of the sidebar: its mark first, then its\n * name or wordmark. It starts where the menu rows start their icons, and the\n * header band around it keeps the top bar's height. On the collapsed icon\n * rail it shows only the mark, in the rail's square like the row icons: the\n * mark keeps its place while the name fades out with the rail's width, as the\n * row labels do. Render it as a link with `render`, such as\n * `render={<a href=\"/\" />}`: it takes the kit's focus outline and, as a logo\n * rather than a row, no hover fill. In a SidebarMenuItem with `expandTrigger`,\n * its mark also expands the collapsed rail.\n */\nexport function SidebarBrand({\n  className,\n  children,\n  render,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  // On the collapsed rail an expand trigger can stand in for the brand, as it\n  // does for a menu button.\n  const stepsAside = React.useContext(SidebarMenuItemContext);\n  // A plain-text name gets a span of its own, so a long one ends with an\n  // ellipsis, keeps its full text in title (K5) and leaves the rail whole.\n  const content = React.Children.map(children, (child) =>\n    typeof child === \"string\" || typeof child === \"number\" ? (\n      <span title={String(child)}>{child}</span>\n    ) : (\n      child\n    ),\n  );\n  const defaultProps = {\n    children: content,\n    className: cn(\n      slotClass(\"sidebar-brand\", props, render),\n      focusRingRecipe(),\n      className,\n    ),\n    \"data-sidebar\": \"brand\",\n    \"data-slot\": \"sidebar-brand\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps(\n      defaultProps,\n      props,\n      stepsAside ? { \"aria-hidden\": true, tabIndex: -1 } : {},\n    ),\n    render,\n  });\n}\n\nexport function SidebarFooter({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  const leadingGutter = React.useContext(SidebarGutterContext);\n\n  return (\n    <div\n      className={cn(\n        slotClass(\"sidebar-footer\", props),\n        \"flex flex-col gap-4 p-2\",\n        sidebarBandRailClassName,\n        leadingGutter && \"ps-0\",\n        className,\n      )}\n      data-sidebar=\"footer\"\n      data-slot=\"sidebar-footer\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator>): React.ReactElement {\n  return (\n    // The line is the kit separator's (data-slot=\"separator\"), like every\n    // other divider; the Sidebar adds only its inset. design-rework.css keeps\n    // the line inside that inset, where the separator's own width of 100 %\n    // ran 8 px past the sidebar.\n    <Separator\n      className={cn(\"mx-2\", className)}\n      data-sidebar=\"separator\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  // A layout reserves a leading gutter on the sidebar. Only then may the scroll\n  // viewport borrow part of it; without one the viewport must stay inside the\n  // sidebar instead of reaching past its edge.\n  const hasLayoutGutter = Boolean(React.useContext(SidebarContext)?.layout);\n\n  return (\n    <ScrollArea\n      className={cn(\n        \"-my-[calc(var(--sp-semantic-state-focus-width)+var(--sp-semantic-state-focus-offset))] min-h-0 w-auto flex-1\",\n        hasLayoutGutter &&\n          \"-ms-[calc(var(--sp-semantic-state-focus-width)+var(--sp-semantic-state-focus-offset))]\",\n      )}\n      fill\n      scrollFade\n    >\n      {/* With a layout gutter, extend into it and restore content alignment.\n          Groups/content already own the trailing inset; keep the scrollbar\n          inside that edge. Block insets protect the first and last outline.\n          The wrapper grows with its content (min-h-full, not h-full), so the\n          trailing block inset stays part of the scroll extent: the last row\n          never ends flush with, or under, the footer band. */}\n      <div\n        className={cn(\n          \"flex min-h-full flex-col py-[calc(var(--sp-semantic-state-focus-width)+var(--sp-semantic-state-focus-offset))]\",\n          hasLayoutGutter &&\n            \"ps-[calc(var(--sp-semantic-state-focus-width)+var(--sp-semantic-state-focus-offset))]\",\n        )}\n      >\n        {/* Groups keep their 8px block padding, so two groups sit 16px\n            apart, like the groups of the footer, without a gap of their\n            own. */}\n        <div\n          className={cn(\n            slotClass(\"sidebar-content\", props),\n            \"flex flex-1 flex-col\",\n            className,\n          )}\n          data-sidebar=\"content\"\n          data-slot=\"sidebar-content\"\n          {...props}\n        />\n      </div>\n    </ScrollArea>\n  );\n}\n\nexport function SidebarGroup({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  const leadingGutter = React.useContext(SidebarGutterContext);\n\n  return (\n    <div\n      className={cn(\n        slotClass(\"sidebar-group\", props),\n        \"relative flex w-full min-w-0 flex-col p-2\",\n        leadingGutter && \"ps-0\",\n        className,\n      )}\n      data-sidebar=\"group\"\n      data-slot=\"sidebar-group\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarGroupLabel({\n  className,\n  render,\n  children,\n  ...props\n}: useRender.ComponentProps<\"div\">): React.ReactElement {\n  // Rendered as a button, the label takes the kit focus outline (T3). On the\n  // collapsed icon rail it fades out and keeps its row, so the groups stay\n  // apart and every icon keeps the place it has in the expanded sidebar.\n  // Rendered as a disabled button, such as a CollapsibleTrigger whose group\n  // cannot fold, it fades as a disabled label does (T1); Base UI keeps such a\n  // trigger focusable with aria-disabled. The rail's fade-out still wins.\n  // A plain-text label gets a span of its own, so a long one ends with an\n  // ellipsis instead of being cut off, with its full text in title (K5).\n  const text = plainText(children);\n  const label =\n    text === undefined ? (\n      children\n    ) : (\n      <span className=\"truncate\" title={text}>\n        {children}\n      </span>\n    );\n  const defaultProps = {\n    children: label,\n    className: cn(\n      slotClass(\"sidebar-group-label\", props, render),\n      focusRingRecipe(),\n      \"flex h-8 shrink-0 items-center overflow-hidden whitespace-nowrap rounded-lg px-2 font-(--sp-rw-font-weight-label) text-sidebar-foreground text-xs transition-opacity duration-(--motion-duration-base) ease-(--motion-ease-linear) [&>svg]:size-4 [&>svg]:shrink-0\",\n      \"group-data-[collapsible=icon]:opacity-0\",\n      \"not-group-data-[collapsible=icon]:disabled:opacity-(--sp-rw-disabled-opacity-action) not-group-data-[collapsible=icon]:aria-disabled:opacity-(--sp-rw-disabled-opacity-action)\",\n      className,\n    ),\n    \"data-sidebar\": \"group-label\",\n    \"data-slot\": \"sidebar-group-label\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps(defaultProps, props),\n    render,\n  });\n}\n\nexport function SidebarGroupAction({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"button\">): React.ReactElement {\n  return (\n    // The 24px target of the control scale (S1), centred where the 20px one\n    // was: on the 32px label row, at its inline end.\n    <KitButton\n      className={cn(\n        \"absolute end-2.5 top-3\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      data-sidebar=\"group-action\"\n      data-slot=\"sidebar-group-action\"\n      profile=\"compact-icon-action\"\n      render={render}\n      size=\"icon-xs\"\n      variant=\"text\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarGroupContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        slotClass(\"sidebar-group-content\", props),\n        \"w-full text-sm\",\n        className,\n      )}\n      data-sidebar=\"group-content\"\n      data-slot=\"sidebar-group-content\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarMenu({\n  className,\n  ...props\n}: React.ComponentProps<\"ul\">): React.ReactElement {\n  return (\n    <ul\n      className={cn(\n        slotClass(\"sidebar-menu\", props),\n        \"flex w-full min-w-0 flex-col gap-1\",\n        className,\n      )}\n      data-sidebar=\"menu\"\n      data-slot=\"sidebar-menu\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarMenuItem({\n  className,\n  children,\n  expandTrigger = false,\n  expandLabel = \"Expand sidebar\",\n  ...props\n}: React.ComponentProps<\"li\"> & {\n  /**\n   * Places the kit's SidebarTrigger over the item's row. On the collapsed icon\n   * rail, hovering or focusing the item swaps the row for the trigger, so the\n   * row (a workspace mark, for example) is also the way to expand. A\n   * SidebarBrand in the item steps aside for the trigger in the same way.\n   */\n  expandTrigger?: boolean;\n  /** The accessible name of the expand trigger. */\n  expandLabel?: string;\n}): React.ReactElement {\n  const sidebar = React.useContext(SidebarContext);\n  const collapsible = React.useContext(SidebarCollapsibleContext);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  // On the collapsed desktop icon rail the row steps aside for the trigger:\n  // the trigger joins the tab order and the row leaves it.\n  const stepsAside =\n    expandTrigger &&\n    collapsible === \"icon\" &&\n    sidebar?.state === \"collapsed\" &&\n    !sidebar.isMobile;\n\n  // Expanding hides the trigger. If it held focus, focus moves to the row or\n  // the brand it stood in for instead of staying on a hidden control.\n  const steppedAside = React.useRef(stepsAside);\n  React.useEffect(() => {\n    const trigger = triggerRef.current;\n    if (\n      steppedAside.current &&\n      !stepsAside &&\n      trigger &&\n      trigger.ownerDocument.activeElement === trigger\n    ) {\n      trigger.parentElement\n        ?.querySelector<HTMLElement>(\n          ':scope > :is([data-sidebar=\"menu-button\"], [data-sidebar=\"brand\"])',\n        )\n        ?.focus();\n    }\n    steppedAside.current = stepsAside;\n  }, [stepsAside]);\n\n  return (\n    <li\n      className={cn(\n        slotClass(\"sidebar-menu-item\", props),\n        \"group/menu-item relative\",\n        className,\n      )}\n      data-expand-trigger={expandTrigger ? \"\" : undefined}\n      data-sidebar=\"menu-item\"\n      data-slot=\"sidebar-menu-item\"\n      {...props}\n    >\n      <SidebarMenuItemContext.Provider value={stepsAside}>\n        {children}\n      </SidebarMenuItemContext.Provider>\n      {expandTrigger ? (\n        // design-rework.css owns the swap, its motion and reduced motion.\n        <SidebarTrigger\n          aria-hidden={stepsAside ? undefined : true}\n          aria-label={expandLabel}\n          className=\"absolute inset-0\"\n          data-sidebar=\"expand-trigger\"\n          ref={triggerRef}\n          tabIndex={stepsAside ? undefined : -1}\n        />\n      ) : null}\n    </li>\n  );\n}\n\nexport function SidebarMenuButton({\n  \"aria-current\": ariaCurrent,\n  active: activeProp,\n  isActive,\n  variant = \"default\",\n  size,\n  tooltip,\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<\"button\"> & {\n  /** Marks the current destination: `aria-current=\"page\"` and the current fill. */\n  active?: boolean;\n  /** @deprecated Use `active`. */\n  isActive?: boolean;\n  tooltip?: string | React.ComponentProps<typeof TooltipPopup>;\n  size?: SidebarMenuButtonSize;\n  variant?: SidebarMenuButtonVariant;\n}): React.ReactElement {\n  const { isMobile, state } = useSidebar();\n  const resolvedSize = useResolvedControlSize(size);\n  const active = activeProp ?? isActive ?? false;\n  // The row of an item whose expand trigger stands in for it on the rail.\n  const stepsAside = React.useContext(SidebarMenuItemContext);\n\n  const buttonElement = (\n    <KitButton\n      align=\"start\"\n      aria-current={ariaCurrent ?? (active ? \"page\" : undefined)}\n      // A menu action or badge at the end of the row keeps the label clear of\n      // it; the row's padding rule reads the end inset (design-rework.css).\n      className={cn(\n        \"group/item peer/menu-button group-has-data-[sidebar=menu-action]/menu-item:[--sp-rw-navigation-row-padding-end:--spacing(10)] group-has-data-[sidebar=menu-badge]/menu-item:[--sp-rw-navigation-row-padding-end:--spacing(10)]\",\n        className,\n      )}\n      // A current row shows the current fill, whether active or\n      // aria-current marks it (T5).\n      data-active={active || isCurrent(ariaCurrent)}\n      data-sidebar=\"menu-button\"\n      data-sidebar-variant={variant}\n      data-slot=\"sidebar-menu-button\"\n      profile=\"navigation-row\"\n      render={render}\n      size={resolvedSize}\n      variant={variant === \"outline\" ? \"outline\" : \"ghost\"}\n      width=\"full\"\n      {...props}\n      {...(stepsAside ? { \"aria-hidden\": true, tabIndex: -1 } : null)}\n      // The row keeps its profile when a trigger renders it, such as a\n      // CollapsibleTrigger that stamps its own profile on what it renders.\n      data-profile=\"navigation-row\"\n    />\n  );\n\n  if (!tooltip) {\n    return buttonElement;\n  }\n\n  const tooltipProps: React.ComponentProps<typeof TooltipPopup> =\n    typeof tooltip === \"string\" ? { children: tooltip } : tooltip;\n\n  // The tooltip wrapper stays mounted while the sidebar collapses or expands,\n  // so the button keeps focus; the tooltip only opens on the collapsed rail,\n  // beside it at its inline end, toward the content in either direction.\n  return (\n    <Tooltip disabled={state !== \"collapsed\" || isMobile}>\n      <TooltipTrigger\n        delay={0}\n        render={buttonElement as React.ReactElement<Record<string, unknown>>}\n      />\n      <TooltipPopup align=\"center\" side=\"inline-end\" {...tooltipProps} />\n    </Tooltip>\n  );\n}\n\n/**\n * The text of a two-line menu row: a title over a quieter description, such as\n * an account's name and email or a workspace's name and plan. Place it in a\n * SidebarMenuButton after the row's icon or avatar; it takes the row's free\n * width, so a trailing chevron stays at the row's end.\n */\nexport function SidebarMenuButtonText({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  return (\n    <span\n      className={cn(slotClass(\"sidebar-menu-button-text\", props), className)}\n      data-sidebar=\"menu-button-text\"\n      data-slot=\"sidebar-menu-button-text\"\n      {...props}\n    />\n  );\n}\n\n/**\n * The first line of a two-line menu row, in the row's own text. A long title\n * ends with an ellipsis; plain text keeps its full value in `title` (K5).\n */\nexport function SidebarMenuButtonTitle({\n  className,\n  children,\n  title,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  return (\n    <span\n      className={cn(slotClass(\"sidebar-menu-button-title\", props), className)}\n      data-sidebar=\"menu-button-title\"\n      data-slot=\"sidebar-menu-button-title\"\n      title={title ?? plainText(children)}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n/**\n * The second line of a two-line menu row: one kit text step smaller than the\n * row, in the tertiary text colour. A long description ends with an ellipsis;\n * plain text keeps its full value in `title` (K5).\n */\nexport function SidebarMenuButtonDescription({\n  className,\n  children,\n  title,\n  ...props\n}: React.ComponentProps<\"span\">): React.ReactElement {\n  return (\n    <span\n      className={cn(\n        slotClass(\"sidebar-menu-button-description\", props),\n        className,\n      )}\n      data-sidebar=\"menu-button-description\"\n      data-slot=\"sidebar-menu-button-description\"\n      title={title ?? plainText(children)}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n/** The text of children made of strings and numbers only, such as `Invoice {id}`. */\nfunction plainText(children: React.ReactNode): string | undefined {\n  const parts = React.Children.toArray(children);\n  const text = parts.every(\n    (part) => typeof part === \"string\" || typeof part === \"number\",\n  )\n    ? parts.join(\"\")\n    : \"\";\n  return text === \"\" ? undefined : text;\n}\n\nexport function SidebarMenuAction({\n  className,\n  showOnHover = false,\n  render,\n  ...props\n}: useRender.ComponentProps<\"button\"> & {\n  showOnHover?: boolean;\n}): React.ReactElement {\n  return (\n    // The kit's bare action (rulebook T2): Button's muted inline action owns\n    // its colours at rest, on hover, on keyboard focus and while its menu is\n    // open, and draws no fill in any state. As a square one it keeps the\n    // square of its size as the pointer target and draws the focus outline\n    // around the glyph (T3).\n    <Button\n      className={cn(\n        // Centred on the row of its menu button at every row size, whatever\n        // its own size (S1). design-rework.css reveals an action that shows\n        // on hover.\n        \"absolute end-1 top-4 z-10 -translate-y-1/2\",\n        \"peer-data-[size=sm]/menu-button:top-3.5\",\n        \"peer-data-[size=lg]/menu-button:top-4.5\",\n        \"peer-data-[size=xl]/menu-button:top-6\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      data-show-on-hover={showOnHover || undefined}\n      data-sidebar=\"menu-action\"\n      data-slot=\"sidebar-menu-action\"\n      profile=\"inline\"\n      profileTone=\"muted\"\n      render={render}\n      shape=\"square\"\n      variant=\"text\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarMenuBadge({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        // The badge sits at the row's inline end, in the room the row keeps\n        // for it, in either direction.\n        slotClass(\"sidebar-menu-badge\", props),\n        \"pointer-events-none absolute end-1 flex h-5 min-w-5 select-none items-center justify-center rounded-lg px-1 font-(--sp-rw-font-weight-label) text-sidebar-foreground text-xs tabular-nums\",\n        \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n        // The 20 px badge centres on rows of 28, 32, 36 and 48 px.\n        \"peer-data-[size=sm]/menu-button:top-1\",\n        \"peer-data-[size=md]/menu-button:top-1.5\",\n        \"peer-data-[size=lg]/menu-button:top-2\",\n        \"peer-data-[size=xl]/menu-button:top-3.5\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      data-sidebar=\"menu-badge\"\n      data-slot=\"sidebar-menu-badge\"\n      {...props}\n    />\n  );\n}\n\n// A loading row is as high as the menu row it stands in for, at every size.\nconst sidebarMenuSkeletonHeightClassNames = {\n  lg: \"h-(--sp-rw-control-lg)\",\n  md: \"h-(--sp-rw-control-md)\",\n  sm: \"h-(--sp-rw-control-sm)\",\n  xl: \"h-(--sp-rw-control-xl)\",\n} as const;\n\nexport function SidebarMenuSkeleton({\n  className,\n  showIcon = false,\n  size,\n  width = \"72%\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showIcon?: boolean;\n  size?: SidebarMenuButtonSize;\n  width?: string;\n}): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  return (\n    <div\n      className={cn(\n        slotClass(\"sidebar-menu-skeleton\", props),\n        \"flex items-center gap-2 rounded-lg px-2\",\n        sidebarMenuSkeletonHeightClassNames[resolvedSize],\n        className,\n      )}\n      data-sidebar=\"menu-skeleton\"\n      data-size={resolvedSize}\n      data-slot=\"sidebar-menu-skeleton\"\n      {...props}\n    >\n      {/* The 20px icon of a menu row. */}\n      {showIcon && (\n        <Skeleton\n          className=\"size-5 rounded-lg\"\n          data-sidebar=\"menu-skeleton-icon\"\n        />\n      )}\n      {/* The width is the run-time value; where a policy blocks the style\n          attribute, the line keeps the default 72%. */}\n      <Skeleton\n        className=\"h-4 max-w-[var(--skeleton-width,72%)] 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\nexport function SidebarMenuSub({\n  className,\n  ...props\n}: React.ComponentProps<\"ul\">): React.ReactElement {\n  return (\n    <ul\n      className={cn(\n        // The guide line is the kit's divider colour, at the inline start\n        // under the row's icon in either direction.\n        slotClass(\"sidebar-menu-sub\", props),\n        \"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-[var(--sp-rw-border-translucent)] border-s px-2.5 py-0.5 rtl:-translate-x-px\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      data-sidebar=\"menu-sub\"\n      data-slot=\"sidebar-menu-sub\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarMenuSubItem({\n  className,\n  ...props\n}: React.ComponentProps<\"li\">): React.ReactElement {\n  return (\n    <li\n      className={cn(\n        slotClass(\"sidebar-menu-sub-item\", props),\n        \"group/menu-sub-item relative\",\n        className,\n      )}\n      data-sidebar=\"menu-sub-item\"\n      data-slot=\"sidebar-menu-sub-item\"\n      {...props}\n    />\n  );\n}\n\nexport function SidebarMenuSubButton({\n  \"aria-current\": ariaCurrent,\n  size,\n  active: activeProp,\n  isActive,\n  className,\n  render,\n  children,\n  ...props\n}: Omit<ButtonBaseProps, \"size\"> & {\n  size?: SidebarMenuButtonSize;\n  /** Marks the current destination: `aria-current=\"page\"` and the current fill. */\n  active?: boolean;\n  /** @deprecated Use `active`. */\n  isActive?: boolean;\n}): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  const active = activeProp ?? isActive ?? false;\n  return (\n    <KitButton\n      align=\"start\"\n      aria-current={ariaCurrent ?? (active ? \"page\" : undefined)}\n      className={cn(\n        \"-translate-x-px rtl:translate-x-px\",\n        \"group-data-[collapsible=icon]:hidden\",\n        className,\n      )}\n      data-active={active || isCurrent(ariaCurrent)}\n      data-sidebar=\"menu-sub-button\"\n      data-slot=\"sidebar-menu-sub-button\"\n      profile=\"navigation-sub-row\"\n      render={render}\n      size={resolvedSize}\n      variant=\"ghost\"\n      width=\"full\"\n      {...props}\n      // The row keeps its profile when a trigger renders it (see\n      // SidebarMenuButton).\n      data-profile=\"navigation-sub-row\"\n    >\n      {/* A plain-text label gets the row's label span, so a long one ends in\n          an ellipsis instead of being cut off (rulebook K5). */}\n      {typeof children === \"string\" || typeof children === \"number\" ? (\n        <span>{children}</span>\n      ) : (\n        children\n      )}\n    </KitButton>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/sidebar/sidebar.tsx"},{"path":"packages/ui/src/components/sidebar/sidebar-internal.ts","content":"import type * as React from \"react\";\n\nimport { SidebarProvider } from \"./sidebar\";\n\n/*\n * Kit-internal SidebarProvider. Not public API: the sidebar index does not\n * export this module, and its names may change in any release. Header\n * Navigation renders its menu as the mobile Sidebar whenever the header itself\n * is narrow, whatever the width of the viewport.\n */\nexport type KitSidebarProviderProps = React.ComponentProps<\n  typeof SidebarProvider\n> & {\n  /** Renders the Sidebar as its mobile sheet at every viewport width. */\n  forceMobile?: boolean | undefined;\n};\n\n/** SidebarProvider with the kit's mobile override; the same component. */\nexport const KitSidebarProvider = SidebarProvider as (\n  props: KitSidebarProviderProps,\n) => React.ReactElement;\n","type":"registry:ui","target":"~/src/siteplane/components/sidebar/sidebar-internal.ts"},{"path":"packages/ui/src/components/sidebar/index.ts","content":"export * from \"./sidebar\";\n","type":"registry:ui","target":"~/src/siteplane/components/sidebar/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/sidebar.md","categories":["Navigation"],"meta":{"avoidWhen":["Avoid for a few top-level pages; a row of links with Navigation Menu is lighter."],"description":"Application navigation sidebar with desktop collapse and a mobile sheet.","knownLimitations":["The desktop state is written to the sidebar_state cookie but not read back; pass defaultOpen from the server or the application. stateCookie renames the cookie, and stateCookie={false} turns it off.","The provider registers Ctrl/Cmd+B unless keyboardShortcut is false."],"providerNotes":["Wrap the sidebar and the content beside it in one SidebarProvider, which holds the open state."],"releaseStatus":"stable","searchTerms":["sidebar"],"useWhen":["Use for the main navigation of an application, such as grouped links with a workspace switcher and an account menu. It collapses on desktop and opens as a sheet on mobile."]}}