# Sidebar
An app's rail: a white column with a rule at its edge, quiet rows, mono group labels and the pale blue current row.

Install: npx opencommerce-ds add sidebar
Requires: theme, button, input, separator, sheet, skeleton, tooltip, utils
Packages: @radix-ui/react-slot, class-variance-authority, lucide-react
Exports: Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar
Client boundary: yes
Reference: Extension from opencommerce-agentcard-web src/app/globals.css (.nav-link, .win)


## Usage
Use for an app's main navigation (an order console, a developer dashboard). Wrap the page in SidebarProvider with the Sidebar and a SidebarInset beside it; put the wordmark or a switcher in SidebarHeader, groups of SidebarMenu rows in SidebarContent and the account in SidebarFooter. Mark the current row with isActive. Set collapsible="icon" to let it narrow to glyphs (give rows a tooltip), floating or inset for the window looks. On phones it opens in the sheet; Cmd or Ctrl and B toggles it.
- Avoid: More than seven top-level rows
- Avoid: Nested rows two levels deep
- Avoid: A sidebar on a marketing page
- Avoid: Icons without labels when expanded


## Examples
Example components: npx opencommerce-ds add sidebar
Example packages: lucide-react
[Site](https://opencommerce-ds.vercel.app/preview/sidebar/Site) | [Icons](https://opencommerce-ds.vercel.app/preview/sidebar/Icons) | [Floating](https://opencommerce-ds.vercel.app/preview/sidebar/Floating) | [Inset](https://opencommerce-ds.vercel.app/preview/sidebar/Inset) | [Loading](https://opencommerce-ds.vercel.app/preview/sidebar/Loading)

examples/sidebar.tsx

```tsx
"use client";

import * as React from "react";
import { BarChart3, BookOpen, ChevronsUpDown, CreditCard, KeyRound, LifeBuoy, PanelLeftClose, Plus, Search, ShoppingBag, Store, Webhook } from "lucide-react";

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";

const MAIN = [
  { id: "orders", label: "Orders", icon: ShoppingBag, count: "12" },
  { id: "merchants", label: "Merchants", icon: Store },
  { id: "cards", label: "Cards", icon: CreditCard },
  { id: "monitoring", label: "Monitoring", icon: BarChart3, mark: "Live" },
];
const DEVELOPERS = [
  { id: "keys", label: "API keys", icon: KeyRound },
  { id: "webhooks", label: "Webhooks", icon: Webhook },
  { id: "docs", label: "Docs", icon: BookOpen },
];

function Wordmark() {
  return (
    <span className="flex items-baseline gap-1.5 px-2 group-data-[collapsible=icon]/sidebar:hidden">
      <span className="font-display text-h5 font-medium tracking-tight text-fg">opencommerce</span>
      <span className="text-caption text-fg-muted">by Agentcard</span>
    </span>
  );
}

function CollapseRow() {
  const { toggleSidebar, state } = useSidebar();
  return (
    <SidebarMenuItem>
      <SidebarMenuButton tooltip="Expand" onClick={toggleSidebar} aria-expanded={state === "expanded"}>
        <PanelLeftClose strokeWidth={1.5} className="transition-[rotate] duration-200 ease-in-out group-data-[collapsible=icon]/sidebar:rotate-180 motion-reduce:transition-none" />
        <span>{state === "expanded" ? "Collapse" : "Expand"}</span>
      </SidebarMenuButton>
    </SidebarMenuItem>
  );
}

function Rail({ collapsible = "offcanvas", variant = "sidebar" }: { collapsible?: "offcanvas" | "icon" | "none"; variant?: "sidebar" | "floating" | "inset" }) {
  const [current, setCurrent] = React.useState("orders");
  const [sub, setSub] = React.useState("all");
  return (
    <Sidebar collapsible={collapsible} variant={variant}>
      <SidebarHeader>
        <Wordmark />
        <SidebarInput placeholder="Search orders" aria-label="Search orders" className="group-data-[collapsible=icon]/sidebar:hidden" />
      </SidebarHeader>
      <SidebarContent>
        <SidebarGroup>
          <SidebarGroupLabel>Commerce</SidebarGroupLabel>
          <SidebarGroupContent>
            <SidebarMenu>
              {MAIN.map((item) => (
                <SidebarMenuItem key={item.id}>
                  <SidebarMenuButton isActive={current === item.id} tooltip={item.label} onClick={() => setCurrent(item.id)}>
                    <item.icon strokeWidth={1.5} />
                    <span>{item.label}</span>
                  </SidebarMenuButton>
                  {item.count && <SidebarMenuBadge>{item.count}</SidebarMenuBadge>}
                  {item.mark && <SidebarMenuBadge variant="mark">{item.mark}</SidebarMenuBadge>}
                  {item.id === "orders" && current === "orders" && (
                    <SidebarMenuSub>
                      {[
                        ["all", "All orders"],
                        ["failed", "Failed checkouts"],
                        ["returns", "Returns"],
                      ].map(([id, label]) => (
                        <SidebarMenuSubItem key={id}>
                          <SidebarMenuSubButton
                            href={`#${id}`}
                            isActive={sub === id}
                            onClick={(event) => {
                              event.preventDefault();
                              setSub(id);
                            }}
                          >
                            <span>{label}</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      ))}
                    </SidebarMenuSub>
                  )}
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
        <SidebarGroup>
          <SidebarGroupLabel>Developers</SidebarGroupLabel>
          <SidebarGroupAction aria-label="Create an API key">
            <Plus strokeWidth={1.5} />
          </SidebarGroupAction>
          <SidebarGroupContent>
            <SidebarMenu>
              {DEVELOPERS.map((item) => (
                <SidebarMenuItem key={item.id}>
                  <SidebarMenuButton isActive={current === item.id} tooltip={item.label} onClick={() => setCurrent(item.id)}>
                    <item.icon strokeWidth={1.5} />
                    <span>{item.label}</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroupContent>
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton tooltip="Help">
              <LifeBuoy strokeWidth={1.5} />
              <span>Help</span>
            </SidebarMenuButton>
          </SidebarMenuItem>
          <SidebarMenuItem>
            <SidebarMenuButton size="lg" tooltip="Acme Labs">
              <span className="grid size-6 shrink-0 place-items-center rounded-sm border border-line-brand-edge bg-primary font-mono text-bar text-on-primary">A</span>
              <span className="grid min-w-0 flex-1 text-left leading-tight">
                <span className="truncate text-body-sm text-fg">Acme Labs</span>
                <span className="truncate font-mono text-bar text-fg-muted">Standard plan</span>
              </span>
              <ChevronsUpDown strokeWidth={1.5} className="ml-auto" />
            </SidebarMenuButton>
          </SidebarMenuItem>
          {collapsible === "icon" && <CollapseRow />}
        </SidebarMenu>
      </SidebarFooter>
      <SidebarRail />
    </Sidebar>
  );
}

function Page({ title }: { title: string }) {
  return (
    <SidebarInset>
      <header className="flex h-14 items-center gap-2 border-b border-line px-4">
        <SidebarTrigger className="-ml-1.5" />
        <p className="font-display text-h5 font-medium tracking-tight text-fg">{title}</p>
        <span className="ml-auto flex items-center gap-2 text-caption text-fg-muted max-sm:hidden">
          <Search strokeWidth={1.5} className="size-3.5" />
          <kbd className="font-mono text-bar">⌘B</kbd> toggles the rail
        </span>
      </header>
      <div className="grid gap-3 p-6">
        {["ord_8KQ2 · Northwind Outfitters · $222.00", "ord_8KR9 · Contoso Coffee · $48.50", "ord_8KS6 · Litware Books · $31.20"].map((row) => (
          <div key={row} className="rounded-control border border-line px-4 py-3 font-mono text-snippet text-fg">
            {row}
          </div>
        ))}
      </div>
    </SidebarInset>
  );
}

export function Site() {
  return (
    <SidebarProvider className="min-h-150 overflow-hidden rounded-panel border border-line-window">
      <Rail />
      <Page title="Orders" />
    </SidebarProvider>
  );
}

export function Icons() {
  return (
    <SidebarProvider defaultOpen={false} className="min-h-150 overflow-hidden rounded-panel border border-line-window">
      <Rail collapsible="icon" />
      <Page title="Orders" />
    </SidebarProvider>
  );
}

export function Floating() {
  return (
    <SidebarProvider className="min-h-150 overflow-hidden rounded-panel border border-line-window bg-subtle">
      <Rail variant="floating" />
      <Page title="Merchants" />
    </SidebarProvider>
  );
}

export function Inset() {
  return (
    <SidebarProvider className="min-h-150 overflow-hidden rounded-panel border border-line-window">
      <Rail variant="inset" />
      <Page title="Monitoring" />
    </SidebarProvider>
  );
}

export function Loading() {
  return (
    <SidebarProvider className="min-h-100 overflow-hidden rounded-panel border border-line-window">
      <Sidebar collapsible="none">
        <SidebarHeader>
          <Wordmark />
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Commerce</SidebarGroupLabel>
            <SidebarMenu>
              {Array.from({ length: 5 }, (_, i) => (
                <SidebarMenuItem key={i}>
                  <SidebarMenuSkeleton showIcon />
                </SidebarMenuItem>
              ))}
            </SidebarMenu>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
      <SidebarInset>
        <div className="grid h-full place-items-center p-6 text-body-sm text-fg-muted">Loading your workspace</div>
      </SidebarInset>
    </SidebarProvider>
  );
}

```

## Source

components/ui/sidebar.tsx

```tsx
"use client";

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeft } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";

// An app's rail, extrapolated from the brand's header links and windows (globals.css .nav-link, .win): 256px of white
// with a 9 percent ink rule at its edge; a 64px head holds the wordmark or a switcher. Rows are 34px with 5px corners,
// 8px in from the rail's sides: a 16px line glyph and a 14px Inter label at 64 percent ink that take a 5 percent ink
// wash and ink text under the pointer (150ms, color only); the current row is the pale blue with blue text. A group's
// label is the window bar's 11.5px mono at 64 percent ink. The foot is ruled off above. `floating` is the rail as one of
// the brand's windows (12px corners, the 16 percent ink edge, the soft drop) 8px from the page's edges; `inset` sets the
// rail on the mist with the page as a white window beside it. Collapsing to icons narrows the rail to 51px (200ms, the
// strong ease-out); Cmd or Ctrl and B toggles it instantly; on a phone it opens in the sheet.
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_ICON = "3.1875rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
const MOBILE_QUERY = "(max-width: 767px)";

type SidebarContextProps = {
  state: "expanded" | "collapsed";
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: boolean;
  instant: boolean;
  toggleSidebar: () => void;
};

const SidebarContext = React.createContext<SidebarContextProps | null>(null);

function useSidebar() {
  const context = React.useContext(SidebarContext);
  if (!context) throw new Error("useSidebar must be used within a SidebarProvider.");
  return context;
}

function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState(false);
  React.useEffect(() => {
    const query = window.matchMedia(MOBILE_QUERY);
    const update = () => setIsMobile(query.matches);
    update();
    query.addEventListener("change", update);
    return () => query.removeEventListener("change", update);
  }, []);
  return isMobile;
}

/**
 * Holds whether the rail is open (expanded or collapsed on a desktop, shown in a sheet on a phone). Uncontrolled with
 * `defaultOpen`, or controlled with `open` and `onOpenChange`. Nothing is stored: keep the state yourself if it should
 * survive a reload. Cmd or Ctrl and B toggles it.
 */
function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }: React.ComponentProps<"div"> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void }) {
  const isMobile = useIsMobile();
  const [openMobile, setOpenMobile] = React.useState(false);
  const [instant, setInstant] = React.useState(false);
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
  const open = openProp ?? internalOpen;
  const setOpen = React.useCallback(
    (value: boolean | ((value: boolean) => boolean)) => {
      const next = typeof value === "function" ? value(open) : value;
      if (setOpenProp) setOpenProp(next);
      else setInternalOpen(next);
    },
    [setOpenProp, open],
  );
  const toggleSidebar = React.useCallback(() => {
    setInstant(false);
    return isMobile ? setOpenMobile((o) => !o) : setOpen((o) => !o);
  }, [isMobile, setOpen]);

  React.useEffect(() => {
    // A shortcut is pressed many times a day: it changes the rail at once, with no motion.
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key !== SIDEBAR_KEYBOARD_SHORTCUT || !(event.metaKey || event.ctrlKey)) return;
      event.preventDefault();
      setInstant(true);
      if (isMobile) setOpenMobile((o) => !o);
      else setOpen((o) => !o);
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [isMobile, setOpen]);

  const state = open ? "expanded" : "collapsed";
  const value = React.useMemo<SidebarContextProps>(() => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, instant }), [state, open, setOpen, isMobile, openMobile, toggleSidebar, instant]);

  return (
    <SidebarContext.Provider value={value}>
      <TooltipProvider delayDuration={0}>
        <div
          data-slot="sidebar-wrapper"
          data-instant={instant || undefined}
          style={{ "--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, ...style } as React.CSSProperties}
          className={cn("group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-subtle", className)}
          {...props}
        >
          {children}
        </div>
      </TooltipProvider>
    </SidebarContext.Provider>
  );
}

/**
 * The rail. `variant`: sidebar (the white rail with its rule), floating (a window 8px from the page's edges) or inset
 * (the rail on the mist with the page as a white window beside it). `collapsible`: offcanvas (slides away), icon
 * (narrows to its glyphs) or none.
 */
function Sidebar({
  side = "left",
  variant = "sidebar",
  collapsible = "offcanvas",
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & { side?: "left" | "right"; variant?: "sidebar" | "floating" | "inset"; collapsible?: "offcanvas" | "icon" | "none" }) {
  const { isMobile, state, openMobile, setOpenMobile, instant } = useSidebar();
  const ground = variant === "inset" ? "bg-subtle" : "bg-surface";

  if (collapsible === "none") {
    return (
      <div data-slot="sidebar" data-variant={variant} className={cn("group/sidebar flex h-full w-(--sidebar-width) flex-col text-fg", ground, variant === "sidebar" && "border-r border-line", className)} {...props}>
        {children}
      </div>
    );
  }

  if (isMobile) {
    return (
      <Sheet open={openMobile} onOpenChange={setOpenMobile}>
        <SheetContent data-slot="sidebar" data-sidebar="sidebar" data-mobile="true" data-variant={variant} side={side} name="navigation" className={cn("group/sidebar w-(--sidebar-width) max-w-[calc(100vw-var(--ds-space-12))] text-fg", ground)}>
          <SheetTitle className="sr-only">Navigation</SheetTitle>
          <SheetDescription className="sr-only">The app's sections and settings.</SheetDescription>
          <div className="flex h-full w-full flex-col">{children}</div>
        </SheetContent>
      </Sheet>
    );
  }

  return (
    <div
      data-slot="sidebar"
      data-state={state}
      data-collapsible={state === "collapsed" ? collapsible : ""}
      data-variant={variant}
      data-side={side}
      className="group/sidebar peer hidden shrink-0 text-fg md:block"
    >
      {/* Holds the rail's room in the row, so the page beside it moves as it opens and closes. */}
      <div
        data-slot="sidebar-gap"
        className={cn(
          "relative h-full w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-out group-data-[collapsible=offcanvas]/sidebar:w-0 group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon) in-data-[instant=true]:transition-none motion-reduce:transition-none",
          (variant === "floating" || variant === "inset") && "group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+var(--ds-space-4))]",
        )}
      >
        <div
          data-slot="sidebar-container"
          className={cn(
            "sticky top-0 z-10 flex h-svh max-h-full w-(--sidebar-width) overflow-hidden transition-[translate,width] duration-200 ease-out in-data-[instant=true]:transition-none motion-reduce:transition-none",
            side === "left" ? "left-0 group-data-[collapsible=offcanvas]/sidebar:-translate-x-full" : "ml-auto group-data-[collapsible=offcanvas]/sidebar:translate-x-full",
            variant === "floating" || variant === "inset" ? "p-2 group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+var(--ds-space-4))]" : "group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon)",
            variant === "sidebar" && (side === "left" ? "border-r border-line" : "border-l border-line"),
            className,
          )}
          {...props}
        >
          <div data-sidebar="sidebar" data-slot="sidebar-inner" className={cn("flex h-full w-full flex-col", ground, variant === "floating" && "overflow-hidden rounded-panel border border-line-window shadow-card")}>
            {children}
          </div>
        </div>
      </div>
    </div>
  );
}

function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
  const { toggleSidebar, open, isMobile, openMobile } = useSidebar();
  return (
    <Button
      data-sidebar="trigger"
      data-slot="sidebar-trigger"
      variant="ghost"
      size="icon"
      aria-expanded={isMobile ? openMobile : open}
      className={cn("text-fg-muted", className)}
      onClick={(event) => {
        onClick?.(event);
        toggleSidebar();
      }}
      {...props}
    >
      <PanelLeft strokeWidth={1.5} className="size-4" />
      <span className="sr-only">Toggle the rail</span>
    </Button>
  );
}

/** A thin strip along the rail's edge that toggles it, with a 2px line in the 16 percent ink under the pointer. */
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
  const { toggleSidebar } = useSidebar();
  return (
    <button
      data-sidebar="rail"
      data-slot="sidebar-rail"
      aria-label="Toggle the rail"
      tabIndex={-1}
      onClick={toggleSidebar}
      title="Toggle the rail"
      className={cn(
        "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-none after:absolute after:inset-y-0 after:left-1/2 after:w-0.5 after:bg-transparent after:transition-colors after:duration-150 after:ease-out hover:after:bg-line-strong sm:flex",
        "in-data-[side=left]:-right-4 in-data-[side=left]:cursor-w-resize in-data-[side=right]:left-0 in-data-[side=right]:cursor-e-resize",
        "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
        className,
      )}
      {...props}
    />
  );
}

/** The page beside the rail. With the inset rail it becomes a white window: 12px corners, the 16 percent ink edge. */
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
  return (
    <main
      data-slot="sidebar-inset"
      className={cn("relative flex w-full min-w-0 flex-1 flex-col bg-surface", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-panel md:peer-data-[variant=inset]:border md:peer-data-[variant=inset]:border-line-window md:peer-data-[variant=inset]:shadow-card", className)}
      {...props}
    />
  );
}

function SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {
  return <Input data-slot="sidebar-input" data-sidebar="input" variant="search" className={cn("w-full", className)} {...props} />;
}

/** The rail's 64px head: the wordmark 16px in, or a switcher. */
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="sidebar-header" data-sidebar="header" className={cn("flex min-h-16 shrink-0 flex-col justify-center gap-2 px-2 py-3", className)} {...props} />;
}

/** The rail's foot (the account, help, Collapse), ruled off above. */
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="sidebar-footer" data-sidebar="footer" className={cn("flex shrink-0 flex-col gap-0.5 border-t border-line p-2", className)} {...props} />;
}

function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
  return <Separator data-slot="sidebar-separator" data-sidebar="separator" className={cn("mx-2 w-auto", className)} {...props} />;
}

function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sidebar-content"
      data-sidebar="content"
      className={cn("flex min-h-0 flex-1 flex-col overflow-auto overscroll-contain [scrollbar-width:none] group-data-[collapsible=icon]/sidebar:overflow-hidden", className)}
      {...props}
    />
  );
}

/** A group of rows. A labeled group keeps 8px above and below it; an unlabeled one (the rows under the head) none. */
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="sidebar-group" data-sidebar="group" className={cn("relative flex w-full min-w-0 flex-col px-2 py-2 [&:not(:has(>[data-slot=sidebar-group-label]))]:py-0", className)} {...props} />;
}

/** A group's label in the window bar's 11.5px mono at 64 percent ink; it folds away when the rail shows glyphs only. */
function SidebarGroupLabel({ className, asChild = false, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "div";
  return (
    <Comp
      data-slot="sidebar-group-label"
      data-sidebar="group-label"
      className={cn(
        "mb-1 flex h-7 shrink-0 items-center px-2.5 font-mono text-bar leading-none whitespace-nowrap text-fg-muted outline-none transition-[margin,opacity] duration-200 ease-out focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-focus-ring group-data-[collapsible=icon]/sidebar:-mt-8 group-data-[collapsible=icon]/sidebar:opacity-0 motion-reduce:transition-none [&>svg]:size-3.5 [&>svg]:shrink-0",
        className,
      )}
      {...props}
    />
  );
}

/** A group's action beside its label (add a card, filter): a 14px glyph in a 24px key at the group's right. */
function SidebarGroupAction({ className, asChild = false, ...props }: React.ComponentProps<"button"> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      data-slot="sidebar-group-action"
      data-sidebar="group-action"
      className={cn(
        "absolute top-2.5 right-3 flex size-6 cursor-pointer items-center justify-center rounded-menu text-fg-muted outline-none transition-[background-color,color] duration-150 ease-out hover:bg-wash-hover hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-focus-ring group-data-[collapsible=icon]/sidebar:hidden motion-reduce:transition-none [&>svg]:size-3.5 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden",
        className,
      )}
      {...props}
    />
  );
}

function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="sidebar-group-content" data-sidebar="group-content" className={cn("w-full", className)} {...props} />;
}

function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
  return <ul data-slot="sidebar-menu" data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-0.5", className)} {...props} />;
}

function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
  return <li data-slot="sidebar-menu-item" data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />;
}

// A row: a 16px glyph 10px in and the label 8px after it. Hover washes the row; the current row is the pale blue with
// blue text. Narrowed to icons, the row is a 34px square around its glyph.
const sidebarMenuButtonVariants = cva(
  "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden rounded-menu pr-2.5 pl-2.5 text-left font-sans text-fg-muted outline-none transition-[background-color,color] duration-150 ease-out group-has-data-[sidebar=menu-action]/menu-item:pr-9 group-data-[collapsible=icon]/sidebar:size-8.5! group-data-[collapsible=icon]/sidebar:justify-center group-data-[collapsible=icon]/sidebar:p-0! group-data-[collapsible=icon]/sidebar:[&>span:not([data-slot=avatar])]:sr-only focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-blue-5 data-[active=true]:text-fg-brand data-[state=open]:bg-wash-hover data-[state=open]:text-fg motion-reduce:transition-none [&>span:last-child]:truncate [&>svg]:shrink-0 [&>svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "hover:bg-wash-hover hover:text-fg data-[active=true]:hover:bg-blue-5 data-[active=true]:hover:text-fg-brand",
        outline: "border border-hairline bg-surface hover:bg-subtle hover:text-fg",
      },
      size: {
        default: "h-8.5 text-body-sm leading-tight",
        sm: "h-7.5 text-caption leading-tight [&>svg:not([class*='size-'])]:size-3.5",
        lg: "h-12 text-body-sm leading-tight group-data-[collapsible=icon]/sidebar:p-0!",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  },
);

function SidebarMenuButton({
  asChild = false,
  isActive = false,
  variant = "default",
  size = "default",
  tooltip,
  className,
  ...props
}: React.ComponentProps<"button"> & { asChild?: boolean; isActive?: boolean; tooltip?: string | React.ComponentProps<typeof TooltipContent> } & VariantProps<typeof sidebarMenuButtonVariants>) {
  const Comp = asChild ? Slot : "button";
  const { isMobile, state } = useSidebar();
  // The tooltip names a row only while the rail shows glyphs alone; otherwise it stays shut, so it never catches Escape.
  const [tipOpen, setTipOpen] = React.useState(false);
  const showTip = state === "collapsed" && !isMobile;
  const button = (
    <Comp
      data-slot="sidebar-menu-button"
      data-sidebar="menu-button"
      data-size={size}
      data-active={isActive || undefined}
      aria-current={isActive ? "page" : undefined}
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
      {...props}
    />
  );
  if (!tooltip) return button;
  const content = typeof tooltip === "string" ? { children: tooltip } : tooltip;
  return (
    <Tooltip open={showTip && tipOpen} onOpenChange={setTipOpen}>
      <TooltipTrigger asChild>{button}</TooltipTrigger>
      <TooltipContent side="right" align="center" {...content} />
    </Tooltip>
  );
}

/** An action at a row's right end (a row's more glyph). `showOnHover` keeps it hidden until the row is hovered or open. */
function SidebarMenuAction({ className, asChild = false, showOnHover = false, ...props }: React.ComponentProps<"button"> & { asChild?: boolean; showOnHover?: boolean }) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      data-slot="sidebar-menu-action"
      data-sidebar="menu-action"
      className={cn(
        "absolute top-1.25 right-1.5 flex size-6 cursor-pointer items-center justify-center rounded-menu text-fg-muted outline-none transition-[background-color,color,opacity] duration-150 ease-out hover:bg-wash-hover hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-focus-ring group-data-[collapsible=icon]/sidebar:hidden motion-reduce:transition-none [&>svg]:size-3.5 [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden",
        "peer-data-[size=sm]/menu-button:top-0.75 peer-data-[size=lg]/menu-button:top-3",
        showOnHover && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-fg-brand md:opacity-0",
        className,
      )}
      {...props}
    />
  );
}

/**
 * A mark at a row's right end, 10px in: `mark` is the brand's chip (the 11.5px mono in blue on the pale blue, 2px
 * corners: "Beta", "Live"); `count` is a quiet number in the mono at 64 percent ink (orders waiting).
 */
function SidebarMenuBadge({ className, variant = "count", ...props }: React.ComponentProps<"div"> & { variant?: "mark" | "count" }) {
  return (
    <div
      data-slot="sidebar-menu-badge"
      data-sidebar="menu-badge"
      className={cn(
        "pointer-events-none absolute top-1.75 right-2.5 flex h-5 items-center justify-center whitespace-nowrap select-none group-data-[collapsible=icon]/sidebar:hidden",
        "peer-data-[size=sm]/menu-button:top-1.25 peer-data-[size=lg]/menu-button:top-3.5",
        variant === "mark" ? "rounded-chip bg-blue-5 px-1.5 font-mono text-bar leading-none text-fg-brand" : "min-w-5 px-1 font-mono text-bar text-fg-muted tabular-nums",
        className,
      )}
      {...props}
    />
  );
}

/** A row's placeholder while the rail loads: a glyph square and a label of varying width. */
function SidebarMenuSkeleton({ className, showIcon = false, ...props }: React.ComponentProps<"div"> & { showIcon?: boolean }) {
  // A width between 50 and 90 percent that differs from row to row but is the same on the server and in the browser.
  const id = React.useId();
  const width = `${50 + (([...id].reduce((sum, c) => (sum * 31 + c.charCodeAt(0)) % 997, 7) % 41))}%`;
  return (
    <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" className={cn("flex h-8.5 items-center gap-2 rounded-menu px-2.5", className)} {...props}>
      {showIcon && <Skeleton className="size-4 rounded-chip" data-sidebar="menu-skeleton-icon" />}
      <Skeleton className="h-3 max-w-(--skeleton-width) flex-1 rounded-chip" data-sidebar="menu-skeleton-text" style={{ "--skeleton-width": width } as React.CSSProperties} />
    </div>
  );
}

/** Rows nested under a row, hung from a 9 percent ink rule under the parent's glyph. */
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
  return (
    <ul
      data-slot="sidebar-menu-sub"
      data-sidebar="menu-sub"
      className={cn("mx-4.5 mt-0.5 flex min-w-0 flex-col gap-0.5 border-l border-line py-0.5 pl-2 group-data-[collapsible=icon]/sidebar:hidden", className)}
      {...props}
    />
  );
}

function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) {
  return <li data-slot="sidebar-menu-sub-item" data-sidebar="menu-sub-item" className={cn("group/menu-sub-item relative", className)} {...props} />;
}

function SidebarMenuSubButton({ asChild = false, size = "md", isActive = false, className, ...props }: React.ComponentProps<"a"> & { asChild?: boolean; size?: "sm" | "md"; isActive?: boolean }) {
  const Comp = asChild ? Slot : "a";
  return (
    <Comp
      data-slot="sidebar-menu-sub-button"
      data-sidebar="menu-sub-button"
      data-size={size}
      data-active={isActive || undefined}
      aria-current={isActive ? "page" : undefined}
      className={cn(
        "flex h-7.5 min-w-0 cursor-pointer items-center gap-2 overflow-hidden rounded-menu px-2.5 font-sans text-fg-muted outline-none transition-[background-color,color] duration-150 ease-out hover:bg-wash-hover hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-blue-5 data-[active=true]:text-fg-brand motion-reduce:transition-none [&>span:last-child]:truncate [&>svg]:size-3.5 [&>svg]:shrink-0",
        size === "sm" ? "text-caption" : "text-body-sm",
        className,
      )}
      {...props}
    />
  );
}

export {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
};

```
