Vela UI Docsvenizia.ai

Sidebar

A collapsible app navigation rail with grouped menus, submenus, and an account card.

npx shadcn@latest add @velaui/sidebar
npx shadcn@latest add @velaui/sidebar

Examples

  • App layout
  • Sidebar floating
  • Sidebar default

View code

Sidebar · App layout

Installation

pnpm dlx velaui add @velaui/sidebar
pnpm dlx velaui add @velaui/sidebar

Code

sidebar-app-layout.tsx
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronSelectorVertical } from "@/components/icons/chevron-selector-vertical";
import { Logout01 } from "@/components/icons/logout-01";
import { UserProfile01 } from "@/components/icons/user-profile-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

// Both menus follow shadcn's TeamSwitcher / NavUser composition as-is — a
// `size="lg"` menu button as the trigger, no layout overrides — so this cell
// shows whether the sidebar's own geometry holds up under the standard pattern.
function WorkspaceSwitcher() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton variant={"outline"} size="lg" tooltip="VelaUi">
              <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
                <Layers01 className="size-6" />
              </div>
              <div className="flex min-w-0 flex-1 flex-col justify-center">
                <span className="truncate text-lg leading-[1.4] font-semibold text-text-strong">
                  VelaUi
                </span>
                <span className="truncate text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                  V1.2
                </span>
              </div>
              <ChevronSelectorVertical className="ml-auto size-5" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="start"
          >
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuItem>
              <Layers01 />
              VelaUi
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Package01 />
              Veni Builder
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Plus01 />
              Add workspace
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

function AccountMenu() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton size="lg" tooltip="Olivia Rhye">
              <Avatar size="sm">
                <AvatarFallback>OR</AvatarFallback>
                <AvatarBadge status="online" />
              </Avatar>
              <div className="flex min-w-0 flex-1 flex-col">
                <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                  Olivia Rhye
                </span>
                <span className="truncate text-sm leading-[1.4] text-text-note">
                  olivia@untitledui.com
                </span>
              </div>
              <ChevronSelectorVertical className="ml-auto size-5" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="end"
          >
            <DropdownMenuLabel>olivia@untitledui.com</DropdownMenuLabel>
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <UserProfile01 />
                Profile
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Settings />
                Settings
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Logout01 />
              Log out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="icon" className="absolute h-full">
          <SidebarHeader>
            <WorkspaceSwitcher />
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    asChild
                    defaultOpen
                    className="group/collapsible"
                  >
                    <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0">
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[state=closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton isActive>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton aria-disabled>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>

                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarContent className="flex-none">
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <AccountMenu />
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronSelectorVertical } from "@/components/icons/chevron-selector-vertical";
import { Logout01 } from "@/components/icons/logout-01";
import { UserProfile01 } from "@/components/icons/user-profile-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

// Both menus follow shadcn's TeamSwitcher / NavUser composition as-is — a
// `size="lg"` menu button as the trigger, no layout overrides — so this cell
// shows whether the sidebar's own geometry holds up under the standard pattern.
function WorkspaceSwitcher() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton variant={"outline"} size="lg" tooltip="VelaUi">
                <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
                  <Layers01 className="size-6" />
                </div>
                <div className="flex min-w-0 flex-1 flex-col justify-center">
                  <span className="truncate text-lg leading-[1.4] font-semibold text-text-strong">
                    VelaUi
                  </span>
                  <span className="truncate text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                    V1.2
                  </span>
                </div>
                <ChevronSelectorVertical className="ml-auto size-5" />
              </SidebarMenuButton>
            }
          />
          <DropdownMenuContent
            className="w-(--anchor-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="start"
          >
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuItem>
              <Layers01 />
              VelaUi
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Package01 />
              Veni Builder
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Plus01 />
              Add workspace
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

function AccountMenu() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton size="lg" tooltip="Olivia Rhye">
                <Avatar size="sm">
                  <AvatarFallback>OR</AvatarFallback>
                  <AvatarBadge status="online" />
                </Avatar>
                <div className="flex min-w-0 flex-1 flex-col">
                  <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                    Olivia Rhye
                  </span>
                  <span className="truncate text-sm leading-[1.4] text-text-note">
                    olivia@untitledui.com
                  </span>
                </div>
                <ChevronSelectorVertical className="ml-auto size-5" />
              </SidebarMenuButton>
            }
          />
          <DropdownMenuContent
            className="w-(--anchor-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="end"
          >
            <DropdownMenuLabel>olivia@untitledui.com</DropdownMenuLabel>
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <UserProfile01 />
                Profile
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Settings />
                Settings
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Logout01 />
              Log out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="icon" className="absolute h-full">
          <SidebarHeader>
            <WorkspaceSwitcher />
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    render={
                      <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0" />
                    }
                    defaultOpen
                    className="group/collapsible"
                  >
                    <CollapsibleTrigger
                      render={
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      }
                    />
                    <CollapsibleContent>
                      <SidebarMenuSub>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton isActive>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton aria-disabled>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      </SidebarMenuSub>
                    </CollapsibleContent>
                  </Collapsible>

                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarContent className="flex-none">
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <AccountMenu />
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

View code

Sidebar · Sidebar floating

Installation

pnpm dlx velaui add @velaui/sidebar
pnpm dlx velaui add @velaui/sidebar

Code

sidebar-sidebar-floating.tsx
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { SortVertical01 } from "@/components/icons/sort-vertical-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarSeparator,
  SidebarTrigger,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar
          variant="floating"
          collapsible="icon"
          className="absolute h-full"
        >
          <SidebarHeader>
            <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
              <Layers01 className="size-6" />
            </div>
            <div className="flex min-w-0 flex-col justify-center group-data-[collapsible=icon]/sidebar:hidden">
              <span className="text-lg leading-[1.4] font-semibold text-text-strong">
                VelaUi
              </span>
              <span className="text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                V1.2
              </span>
            </div>
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    asChild
                    defaultOpen
                    className="group/collapsible"
                  >
                    <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0">
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[state=closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton isActive>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton aria-disabled>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton size="lg">
                  <Avatar size="sm">
                    <AvatarFallback>OR</AvatarFallback>
                    <AvatarBadge status="online" />
                  </Avatar>
                  <div className="flex min-w-0 flex-col group-data-[collapsible=icon]/sidebar:hidden">
                    <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                      Olivia Rhye
                    </span>
                    <span className="truncate text-sm leading-[1.4] text-text-note">
                      olivia@untitledui.com
                    </span>
                  </div>

                  {/* The whole row is the account switcher; the glyph is its
                      affordance, not a second button nested inside it. */}
                  <span
                    aria-hidden="true"
                    className="inline-flex size-8 shrink-0 items-center justify-center text-text-strong group-data-[collapsible=icon]/sidebar:hidden"
                  >
                    <SortVertical01 className="size-5" />
                  </span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { SortVertical01 } from "@/components/icons/sort-vertical-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarSeparator,
  SidebarTrigger,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar
          variant="floating"
          collapsible="icon"
          className="absolute h-full"
        >
          <SidebarHeader>
            <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
              <Layers01 className="size-6" />
            </div>
            <div className="flex min-w-0 flex-col justify-center group-data-[collapsible=icon]/sidebar:hidden">
              <span className="text-lg leading-[1.4] font-semibold text-text-strong">
                VelaUi
              </span>
              <span className="text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                V1.2
              </span>
            </div>
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    render={
                      <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0" />
                    }
                    defaultOpen
                    className="group/collapsible"
                  >
                    <CollapsibleTrigger
                      render={
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      }
                    />
                    <CollapsibleContent>
                      <SidebarMenuSub>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton isActive>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton aria-disabled>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      </SidebarMenuSub>
                    </CollapsibleContent>
                  </Collapsible>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton size="lg">
                  <Avatar size="sm">
                    <AvatarFallback>OR</AvatarFallback>
                    <AvatarBadge status="online" />
                  </Avatar>
                  <div className="flex min-w-0 flex-col group-data-[collapsible=icon]/sidebar:hidden">
                    <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                      Olivia Rhye
                    </span>
                    <span className="truncate text-sm leading-[1.4] text-text-note">
                      olivia@untitledui.com
                    </span>
                  </div>

                  {/* The whole row is the account switcher; the glyph is its
                      affordance, not a second button nested inside it. */}
                  <span
                    aria-hidden="true"
                    className="inline-flex size-8 shrink-0 items-center justify-center text-text-strong group-data-[collapsible=icon]/sidebar:hidden"
                  >
                    <SortVertical01 className="size-5" />
                  </span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

View code

Sidebar · Sidebar default

Installation

pnpm dlx velaui add @velaui/sidebar
pnpm dlx velaui add @velaui/sidebar

Code

sidebar-sidebar-default.tsx
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { SortVertical01 } from "@/components/icons/sort-vertical-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Separator } from "@/components/ui/separator";

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar className="absolute h-full">
          <SidebarHeader>
            <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
              <Layers01 className="size-6" />
            </div>
            <div className="flex min-w-0 flex-col justify-center group-data-[collapsible=icon]/sidebar:hidden">
              <span className="text-lg leading-[1.4] font-semibold text-text-strong">
                VelaUi
              </span>
              <span className="text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                V1.2
              </span>
            </div>
            {/* Centred with a margin, not -translate-y-1/2: Button presses
                with `active:translate-y-px`, which would replace a transform. */}
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    asChild
                    defaultOpen
                    className="group/collapsible"
                  >
                    <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0">
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[state=closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton isActive>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton aria-disabled>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton>
                  <Avatar size="sm">
                    <AvatarFallback>OR</AvatarFallback>
                    <AvatarBadge status="online" />
                  </Avatar>
                  <div className="flex min-w-0 flex-col group-data-[collapsible=icon]/sidebar:hidden">
                    <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                      Olivia Rhye
                    </span>
                    <span className="truncate text-sm leading-[1.4] text-text-note">
                      olivia@untitledui.com
                    </span>
                  </div>

                  {/* The whole row is the account switcher; the glyph is its
                      affordance, not a second button nested inside it. */}
                  <span
                    aria-hidden="true"
                    className="inline-flex size-8 shrink-0 items-center justify-center text-text-strong group-data-[collapsible=icon]/sidebar:hidden"
                  >
                    <SortVertical01 className="size-5" />
                  </span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 overflow-hidden">
          {/* An offcanvas panel slides fully out of view, so the toggle lives
              in the page header — the one spot that is always on screen. */}
          <header className="flex h-20 shrink-0 items-center gap-4 border-b border-border-default px-6">
            <SidebarTrigger />
            <Separator orientation="vertical" emphasis={'light'} className="h-full" />
            <div className="h-4 w-40 animate-pulse rounded bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </header>

          <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-auto p-6">
            <div className="grid grid-cols-3 gap-4">
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            </div>
            <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="flex flex-col gap-3">
              <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
              <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
              <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
            </div>
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { SortVertical01 } from "@/components/icons/sort-vertical-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Separator } from "@/components/ui/separator";

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar className="absolute h-full">
          <SidebarHeader>
            <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
              <Layers01 className="size-6" />
            </div>
            <div className="flex min-w-0 flex-col justify-center group-data-[collapsible=icon]/sidebar:hidden">
              <span className="text-lg leading-[1.4] font-semibold text-text-strong">
                VelaUi
              </span>
              <span className="text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                V1.2
              </span>
            </div>
            {/* Centred with a margin, not -translate-y-1/2: Button presses
                with `active:translate-y-px`, which would replace a transform. */}
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    render={
                      <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0" />
                    }
                    defaultOpen
                    className="group/collapsible"
                  >
                    <CollapsibleTrigger
                      render={
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      }
                    />
                    <CollapsibleContent>
                      <SidebarMenuSub>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton isActive>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton aria-disabled>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      </SidebarMenuSub>
                    </CollapsibleContent>
                  </Collapsible>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton>
                  <Avatar size="sm">
                    <AvatarFallback>OR</AvatarFallback>
                    <AvatarBadge status="online" />
                  </Avatar>
                  <div className="flex min-w-0 flex-col group-data-[collapsible=icon]/sidebar:hidden">
                    <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                      Olivia Rhye
                    </span>
                    <span className="truncate text-sm leading-[1.4] text-text-note">
                      olivia@untitledui.com
                    </span>
                  </div>

                  {/* The whole row is the account switcher; the glyph is its
                      affordance, not a second button nested inside it. */}
                  <span
                    aria-hidden="true"
                    className="inline-flex size-8 shrink-0 items-center justify-center text-text-strong group-data-[collapsible=icon]/sidebar:hidden"
                  >
                    <SortVertical01 className="size-5" />
                  </span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 overflow-hidden">
          {/* An offcanvas panel slides fully out of view, so the toggle lives
              in the page header — the one spot that is always on screen. */}
          <header className="flex h-20 shrink-0 items-center gap-4 border-b border-border-default px-6">
            <SidebarTrigger />
            <Separator orientation="vertical" emphasis={'light'} className="h-full" />
            <div className="h-4 w-40 animate-pulse rounded bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </header>

          <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-auto p-6">
            <div className="grid grid-cols-3 gap-4">
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
              <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            </div>
            <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="flex flex-col gap-3">
              <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
              <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
              <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
            </div>
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

Usage

import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronSelectorVertical } from "@/components/icons/chevron-selector-vertical";
import { Logout01 } from "@/components/icons/logout-01";
import { UserProfile01 } from "@/components/icons/user-profile-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

// Both menus follow shadcn's TeamSwitcher / NavUser composition as-is — a
// `size="lg"` menu button as the trigger, no layout overrides — so this cell
// shows whether the sidebar's own geometry holds up under the standard pattern.
function WorkspaceSwitcher() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton variant={"outline"} size="lg" tooltip="VelaUi">
              <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
                <Layers01 className="size-6" />
              </div>
              <div className="flex min-w-0 flex-1 flex-col justify-center">
                <span className="truncate text-lg leading-[1.4] font-semibold text-text-strong">
                  VelaUi
                </span>
                <span className="truncate text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                  V1.2
                </span>
              </div>
              <ChevronSelectorVertical className="ml-auto size-5" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="start"
          >
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuItem>
              <Layers01 />
              VelaUi
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Package01 />
              Veni Builder
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Plus01 />
              Add workspace
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

function AccountMenu() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <SidebarMenuButton size="lg" tooltip="Olivia Rhye">
              <Avatar size="sm">
                <AvatarFallback>OR</AvatarFallback>
                <AvatarBadge status="online" />
              </Avatar>
              <div className="flex min-w-0 flex-1 flex-col">
                <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                  Olivia Rhye
                </span>
                <span className="truncate text-sm leading-[1.4] text-text-note">
                  olivia@untitledui.com
                </span>
              </div>
              <ChevronSelectorVertical className="ml-auto size-5" />
            </SidebarMenuButton>
          </DropdownMenuTrigger>
          <DropdownMenuContent
            className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="end"
          >
            <DropdownMenuLabel>olivia@untitledui.com</DropdownMenuLabel>
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <UserProfile01 />
                Profile
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Settings />
                Settings
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Logout01 />
              Log out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="icon" className="absolute h-full">
          <SidebarHeader>
            <WorkspaceSwitcher />
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    asChild
                    defaultOpen
                    className="group/collapsible"
                  >
                    <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0">
                      <CollapsibleTrigger asChild>
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[state=closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      </CollapsibleTrigger>
                      <CollapsibleContent>
                        <SidebarMenuSub>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton isActive>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                          <SidebarMenuSubItem>
                            <SidebarMenuSubButton aria-disabled>
                              <span>Sub Label</span>
                            </SidebarMenuSubButton>
                          </SidebarMenuSubItem>
                        </SidebarMenuSub>
                      </CollapsibleContent>
                    </SidebarMenuItem>
                  </Collapsible>

                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarContent className="flex-none">
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <AccountMenu />
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}
import { Bell01 } from "@/components/icons/bell-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { ChevronSelectorVertical } from "@/components/icons/chevron-selector-vertical";
import { Logout01 } from "@/components/icons/logout-01";
import { UserProfile01 } from "@/components/icons/user-profile-01";
import { Home01 } from "@/components/icons/home-01";
import { Hourglass01 } from "@/components/icons/hourglass-01";
import { Layers01 } from "@/components/icons/layers-01";
import { Map01 } from "@/components/icons/map-01";
import { DotHorizontal } from "@/components/icons/dot-horizontal";
import { Package01 } from "@/components/icons/package-01";
import { Plus01 } from "@/components/icons/plus-01";
import { Settings } from "@/components/icons/settings";
import { Shield01 } from "@/components/icons/shield-01";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupAction,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar";
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
} from "@/components/ui/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

// Both menus follow shadcn's TeamSwitcher / NavUser composition as-is — a
// `size="lg"` menu button as the trigger, no layout overrides — so this cell
// shows whether the sidebar's own geometry holds up under the standard pattern.
function WorkspaceSwitcher() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton variant={"outline"} size="lg" tooltip="VelaUi">
                <div className="flex size-10 shrink-0 items-center justify-center rounded bg-bg-foreground text-icon-inverse">
                  <Layers01 className="size-6" />
                </div>
                <div className="flex min-w-0 flex-1 flex-col justify-center">
                  <span className="truncate text-lg leading-[1.4] font-semibold text-text-strong">
                    VelaUi
                  </span>
                  <span className="truncate text-sm leading-5 font-medium tracking-[-0.2px] text-text-note">
                    V1.2
                  </span>
                </div>
                <ChevronSelectorVertical className="ml-auto size-5" />
              </SidebarMenuButton>
            }
          />
          <DropdownMenuContent
            className="w-(--anchor-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="start"
          >
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuItem>
              <Layers01 />
              VelaUi
            </DropdownMenuItem>
            <DropdownMenuItem>
              <Package01 />
              Veni Builder
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Plus01 />
              Add workspace
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

function AccountMenu() {
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton size="lg" tooltip="Olivia Rhye">
                <Avatar size="sm">
                  <AvatarFallback>OR</AvatarFallback>
                  <AvatarBadge status="online" />
                </Avatar>
                <div className="flex min-w-0 flex-1 flex-col">
                  <span className="truncate text-sm leading-[1.4] font-semibold text-text-strong">
                    Olivia Rhye
                  </span>
                  <span className="truncate text-sm leading-[1.4] text-text-note">
                    olivia@untitledui.com
                  </span>
                </div>
                <ChevronSelectorVertical className="ml-auto size-5" />
              </SidebarMenuButton>
            }
          />
          <DropdownMenuContent
            className="w-(--anchor-width) min-w-56"
            side={isMobile ? "bottom" : "right"}
            align="end"
          >
            <DropdownMenuLabel>olivia@untitledui.com</DropdownMenuLabel>
            <DropdownMenuGroup>
              <DropdownMenuItem>
                <UserProfile01 />
                Profile
              </DropdownMenuItem>
              <DropdownMenuItem>
                <Settings />
                Settings
              </DropdownMenuItem>
            </DropdownMenuGroup>
            <DropdownMenuSeparator />
            <DropdownMenuItem>
              <Logout01 />
              Log out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}

export default function Example() {
  return (
    // `transform-gpu` makes this box the containing block for the panel's
    // fixed positioning, so the app shell stays inside the docs frame.
    <div className="h-200 w-full transform-gpu overflow-hidden rounded-xl border border-border-default">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="icon" className="absolute h-full">
          <SidebarHeader>
            <WorkspaceSwitcher />
            {/* Centred ON the header/body boundary, not within the header:
                checked directly against the Figma node tree, the button's
                CENTER (not top) lands exactly on the divider between the
                header and body slots, in both the Full and Mini examples. */}
            <SidebarTrigger className="absolute z-30 top-full -right-3 -mt-3" />
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup className="group-data-[collapsible=icon]/sidebar:hidden">
              <SidebarInput type="search" placeholder="Search…" />
            </SidebarGroup>

            <SidebarGroup>
              <SidebarGroupLabel>Dashboard</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive tooltip="Home">
                      <Home01 />
                      <span>Home</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Subscriptions">
                      <Shield01 />
                      <span>Subscriptions</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Transactions">
                      <Hourglass01 />
                      <span>Transactions</span>
                    </SidebarMenuButton>
                    <SidebarMenuBadge>123</SidebarMenuBadge>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="SMS">
                      <Map01 />
                      <span>SMS</span>
                    </SidebarMenuButton>
                    <SidebarMenuAction showOnHover aria-label="More options">
                      <DotHorizontal />
                    </SidebarMenuAction>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton disabled tooltip="Reports">
                      <Bell01 />
                      <span>Reports</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>

            <SidebarSeparator />

            <SidebarGroup>
              <SidebarGroupLabel>Pages</SidebarGroupLabel>
              <SidebarGroupAction aria-label="Add page">
                <Plus01 />
              </SidebarGroupAction>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Settings">
                      <Settings />
                      <span>Settings</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  {/* An expanded parent keeps the surface behind its sub-list. */}
                  <Collapsible
                    render={
                      <SidebarMenuItem className="rounded-xl data-open:bg-bg-surface-1 data-open:pb-2 group-data-[collapsible=icon]/sidebar:bg-transparent group-data-[collapsible=icon]/sidebar:pb-0" />
                    }
                    defaultOpen
                    className="group/collapsible"
                  >
                    <CollapsibleTrigger
                      render={
                        <SidebarMenuButton tooltip="Label">
                          <Home01 />
                          <span>Label</span>
                          <ChevronDown className="ml-auto transition-transform duration-200 group-data-[closed]/collapsible:-rotate-90 group-data-[collapsible=icon]/sidebar:hidden" />
                        </SidebarMenuButton>
                      }
                    />
                    <CollapsibleContent>
                      <SidebarMenuSub>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton isActive>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                        <SidebarMenuSubItem>
                          <SidebarMenuSubButton aria-disabled>
                            <span>Sub Label</span>
                          </SidebarMenuSubButton>
                        </SidebarMenuSubItem>
                      </SidebarMenuSub>
                    </CollapsibleContent>
                  </Collapsible>

                  {/* Rows still loading from the server. */}
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuSkeleton showIcon />
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>

          <SidebarContent className="flex-none">
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton tooltip="Packages">
                      <Package01 />
                      <span>Packages</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <AccountMenu />
          </SidebarFooter>

          <SidebarRail />
        </Sidebar>

        {/* The page — placeholder blocks stand in for real content. */}
        <SidebarInset className="min-w-0 gap-6 overflow-auto p-6">
          <div className="flex items-center gap-4">
            <div className="h-8 w-48 animate-pulse rounded-lg bg-bg-surface-1" />
            <div className="ml-auto h-8 w-24 animate-pulse rounded-lg bg-bg-surface-1" />
          </div>
          <div className="grid grid-cols-3 gap-4">
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
            <div className="h-24 animate-pulse rounded-xl bg-bg-surface-1" />
          </div>
          <div className="h-56 animate-pulse rounded-xl bg-bg-surface-1" />
          <div className="flex flex-col gap-3">
            <div className="h-4 w-full animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-bg-surface-1" />
            <div className="h-4 w-2/3 animate-pulse rounded bg-bg-surface-1" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

Props

Sidebar

PropTypeDefault
collapsible"none" | "icon" | "offcanvas""offcanvas"
side"left" | "right""left"
variant"sidebar" | "floating" | "inset""sidebar"

Takes these and every prop of SidebarProps.

SidebarContent

Takes every prop of SidebarContentProps.

SidebarFooter

Takes every prop of SidebarFooterProps.

SidebarGroup

Takes every prop of SidebarGroupProps.

SidebarGroupAction

PropTypeDefault
asChildbooleanfalse

Takes these and every prop of SidebarGroupActionProps.

SidebarGroupContent

Takes every prop of SidebarGroupContentProps.

SidebarGroupLabel

PropTypeDefault
asChildbooleanfalse

Takes these and every prop of SidebarGroupLabelProps.

SidebarHeader

Takes every prop of SidebarHeaderProps.

SidebarInput

PropTypeDefault
size"sm" | "md" | "lg"—
variant"fill" | "underline" | "outlined"—

Takes these and every prop of SidebarInputProps.

SidebarInset

Takes every prop of SidebarInsetProps.

SidebarMenu

Takes every prop of SidebarMenuProps.

SidebarMenuAction

PropTypeDefault
asChildbooleanfalse
showOnHoverbooleanfalse

Takes these and every prop of SidebarMenuActionProps.

SidebarMenuBadge

Takes every prop of SidebarMenuBadgeProps.

SidebarMenuButton

PropTypeDefault
asChildbooleanfalse
isActivebooleanfalse
size"sm" | "lg" | "default""default"
tooltipstring | (import("/app/node_modules/.bun/@radix-ui+react-tooltip@1.2.10+ae2b4604ef263bc9/node_modules/@radix-ui/react-tooltip/dist/index").TooltipContentProps & React.RefAttributes<HTMLDivElement> & VariantProps<(props?: ({ variant?: "neutral" | "accent" | null | undefined; } & import("/app/node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types").ClassProp) | undefined) => string>)—
variant"outline" | "default""default"

Takes these and every prop of SidebarMenuButtonProps.

SidebarMenuItem

Takes every prop of SidebarMenuItemProps.

SidebarMenuSkeleton

PropTypeDefault
showIconbooleanfalse

Takes these and every prop of SidebarMenuSkeletonProps.

SidebarMenuSub

Takes every prop of SidebarMenuSubProps.

SidebarMenuSubButton

PropTypeDefault
asChildbooleanfalse
isActivebooleanfalse
size"sm" | "md""md"

Takes these and every prop of SidebarMenuSubButtonProps.

SidebarMenuSubItem

Takes every prop of SidebarMenuSubItemProps.

SidebarProvider

PropTypeDefault
defaultOpenbooleantrue
onOpenChange(open: boolean) => void—
openboolean—

Takes these and every prop of SidebarProviderProps.

SidebarRail

Takes every prop of SidebarRailProps.

SidebarSeparator

PropTypeDefault
emphasis"high" | "light"—
thickness"default" | "bold"—
variant"solid" | "dash"—

Takes these and every prop of SidebarSeparatorProps.

SidebarTrigger

PropTypeDefault
asChildboolean—
size"sm" | "md" | "lg" | "xs" | "xl" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl"—
variant"outline" | "link" | "destructive" | "ghost" | "primary" | "secondary"—

Takes these and every prop of SidebarTriggerProps.

Sidebar

PropTypeDefault
collapsible"none" | "icon" | "offcanvas""offcanvas"
side"left" | "right""left"
variant"sidebar" | "floating" | "inset""sidebar"

Takes these and every prop of SidebarProps.

SidebarContent

Takes every prop of SidebarContentProps.

SidebarFooter

Takes every prop of SidebarFooterProps.

SidebarGroup

Takes every prop of SidebarGroupProps.

SidebarGroupAction

Takes every prop of SidebarGroupActionProps.

SidebarGroupContent

Takes every prop of SidebarGroupContentProps.

SidebarGroupLabel

Takes every prop of SidebarGroupLabelProps.

SidebarHeader

Takes every prop of SidebarHeaderProps.

SidebarInput

PropTypeDefault
size"sm" | "md" | "lg"—
variant"underline" | "fill" | "outlined"—

Takes these and every prop of SidebarInputProps.

SidebarInset

Takes every prop of SidebarInsetProps.

SidebarMenu

Takes every prop of SidebarMenuProps.

SidebarMenuAction

PropTypeDefault
showOnHoverbooleanfalse

Takes these and every prop of SidebarMenuActionProps.

SidebarMenuBadge

Takes every prop of SidebarMenuBadgeProps.

SidebarMenuButton

PropTypeDefault
isActivebooleanfalse
size"sm" | "lg" | "default""default"
tooltipstring | (import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/index").TooltipPopupProps & Pick<import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/index").TooltipPositionerProps, "side" | "align" | "alignOffset" | "arrowPadding"> & VariantProps<(props?: ({ variant?: "neutral" | "accent" | null | undefined; } & import("/app/node_modules/.bun/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types").ClassProp) | undefined) => string> & { sideOffset?: number; })—
variant"outline" | "default""default"

Takes these and every prop of SidebarMenuButtonProps.

SidebarMenuItem

Takes every prop of SidebarMenuItemProps.

SidebarMenuSkeleton

PropTypeDefault
showIconbooleanfalse

Takes these and every prop of SidebarMenuSkeletonProps.

SidebarMenuSub

Takes every prop of SidebarMenuSubProps.

SidebarMenuSubButton

PropTypeDefault
isActivebooleanfalse
size"sm" | "md""md"

Takes these and every prop of SidebarMenuSubButtonProps.

SidebarMenuSubItem

Takes every prop of SidebarMenuSubItemProps.

SidebarProvider

PropTypeDefault
defaultOpenbooleantrue
onOpenChange(open: boolean) => void—
openboolean—

Takes these and every prop of SidebarProviderProps.

SidebarRail

Takes every prop of SidebarRailProps.

SidebarSeparator

PropTypeDefault
emphasis"high" | "light"—
thickness"default" | "bold"—
variant"solid" | "dash"—

Takes these and every prop of SidebarSeparatorProps.

SidebarTrigger

PropTypeDefault
size"sm" | "md" | "lg" | "xs" | "xl" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl"—
variant"link" | "outline" | "destructive" | "ghost" | "primary" | "secondary"—

Takes these and every prop of SidebarTriggerProps.

Dependencies

npm
class-variance-authority@0.7.1radix-ui@1.6.0
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1

Source

components/ui/sidebar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Dialog as DialogPrimitive, Slot } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronLeft } from "@/components/icons/chevron-left";
import { ChevronRight } from "@/components/icons/chevron-right";

import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
/** Full and mini rail widths are drawn at 292px / 80px in the design. */
const SIDEBAR_WIDTH = "292px";
const SIDEBAR_WIDTH_MOBILE = "292px";
const SIDEBAR_WIDTH_ICON = "80px";
/**
 * The mini rail's square: every collapsed row, logo and avatar slot is this
 * size, centred by an equal gutter on each side. Keep the two in one equation
 * — changing the rail or the square re-derives the gutter for every slot.
 */
const SIDEBAR_ITEM = "40px";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";

export type SidebarContextProps = {
  state: "expanded" | "collapsed";
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: 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;
}

export type SidebarProviderProps = React.ComponentProps<"div"> & {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
};

function SidebarProvider({
  defaultOpen = true,
  open: openProp,
  onOpenChange: setOpenProp,
  className,
  style,
  children,
  ...props
}: SidebarProviderProps) {
  const isMobile = useIsMobile();
  const [openMobile, setOpenMobile] = React.useState(false);

  const [_open, _setOpen] = React.useState(defaultOpen);
  const open = openProp ?? _open;
  const setOpen = React.useCallback(
    (value: boolean | ((value: boolean) => boolean)) => {
      const openState = typeof value === "function" ? value(open) : value;
      if (setOpenProp) {
        setOpenProp(openState);
      } else {
        _setOpen(openState);
      }

      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
    },
    [setOpenProp, open],
  );

  const toggleSidebar = React.useCallback(() => {
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
  }, [isMobile, setOpen, setOpenMobile]);

  React.useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (
        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
        (event.metaKey || event.ctrlKey)
      ) {
        event.preventDefault();
        toggleSidebar();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [toggleSidebar]);

  const state = open ? "expanded" : "collapsed";

  const contextValue = React.useMemo<SidebarContextProps>(
    () => ({
      state,
      open,
      setOpen,
      isMobile,
      openMobile,
      setOpenMobile,
      toggleSidebar,
    }),
    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
  );

  return (
    <SidebarContext value={contextValue}>
      <TooltipProvider delayDuration={0}>
        <div
          data-slot="sidebar-wrapper"
          style={
            {
              "--sidebar-width": SIDEBAR_WIDTH,
              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
              "--sidebar-item": SIDEBAR_ITEM,
              "--sidebar-gutter":
                "calc((var(--sidebar-width-icon) - var(--sidebar-item)) / 2)",
              ...style,
            } as React.CSSProperties
          }
          className={cn(
            "group/sidebar-wrapper flex min-h-svh w-full bg-bg-main",
            className,
          )}
          {...props}
        >
          {children}
        </div>
      </TooltipProvider>
    </SidebarContext>
  );
}

/** The panel body — the surface every layout variant renders its children into. */
const SIDEBAR_PANEL = "flex size-full flex-col bg-bg-main text-text-strong";

export type SidebarProps = React.ComponentProps<"div"> & {
  side?: "left" | "right";
  variant?: "sidebar" | "floating" | "inset";
  collapsible?: "offcanvas" | "icon" | "none";
};

function Sidebar({
  side = "left",
  variant = "sidebar",
  collapsible = "offcanvas",
  className,
  children,
  ...props
}: SidebarProps) {
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();

  if (collapsible === "none") {
    return (
      <div
        data-slot="sidebar"
        data-state="expanded"
        className={cn(
          "group/sidebar w-(--sidebar-width)",
          SIDEBAR_PANEL,
          className,
        )}
        {...props}
      >
        {children}
      </div>
    );
  }

  if (isMobile) {
    return (
      <DialogPrimitive.Root open={openMobile} onOpenChange={setOpenMobile}>
        <DialogPrimitive.Portal>
          <DialogPrimitive.Overlay
            data-slot="sidebar-overlay"
            className="fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
          />
          <DialogPrimitive.Content
            data-slot="sidebar"
            data-mobile="true"
            data-side={side}
            data-state={state}
            className={cn(
              "group/sidebar fixed inset-y-0 z-50 w-(--sidebar-width) outline-none duration-200 data-open:animate-in data-closed:animate-out",
              side === "left"
                ? "left-0 data-open:slide-in-from-left data-closed:slide-out-to-left"
                : "right-0 data-open:slide-in-from-right data-closed:slide-out-to-right",
              className,
            )}
            style={
              {
                "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
              } as React.CSSProperties
            }
            {...props}
          >
            <DialogPrimitive.Title className="sr-only">
              Sidebar
            </DialogPrimitive.Title>
            <DialogPrimitive.Description className="sr-only">
              Displays the mobile sidebar.
            </DialogPrimitive.Description>
            <div className={SIDEBAR_PANEL}>{children}</div>
          </DialogPrimitive.Content>
        </DialogPrimitive.Portal>
      </DialogPrimitive.Root>
    );
  }

  return (
    <div
      className="group/sidebar peer hidden text-text-strong md:block"
      data-state={state}
      data-collapsible={state === "collapsed" ? collapsible : ""}
      data-variant={variant}
      data-side={side}
      data-slot="sidebar"
    >
      {/* Holds the space the fixed panel occupies in the page flow. */}
      <div
        data-slot="sidebar-gap"
        className={cn(
          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
          "group-data-[collapsible=offcanvas]/sidebar:w-0",
          "group-data-[side=right]/sidebar:rotate-180",
          variant === "floating" || variant === "inset"
            ? "group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
            : "group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon)",
        )}
      />
      <div
        data-slot="sidebar-container"
        data-side={side}
        className={cn(
          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]/sidebar:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]/sidebar:right-[calc(var(--sidebar-width)*-1)] md:flex",
          variant === "floating" || variant === "inset"
            ? "p-2 group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
            : // The design's stroke on the panel has no per-side override, so
              // it runs on all four edges, not just the trailing one.
              "group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon) border border-border-default",
          className,
        )}
        {...props}
      >
        <div
          data-slot="sidebar-inner"
          className={cn(
            SIDEBAR_PANEL,
            "group-data-[variant=floating]/sidebar:rounded-xl group-data-[variant=floating]/sidebar:border group-data-[variant=floating]/sidebar:border-border-default",
          )}
        >
          {children}
        </div>
      </div>
    </div>
  );
}

export type SidebarTriggerProps = React.ComponentProps<typeof Button>;

/**
 * The 24px collapse pill. The design pins it half-outside the panel's trailing
 * edge, which is a placement decision for the layout — pass it as `className`.
 *
 * Position it with insets or margins, never a transform: Button presses with
 * `active:translate-y-px`, and that active utility replaces the whole
 * `translate` property, so a centring `-translate-y-1/2` would drop out on
 * mousedown and the pill would jump by half its height.
 */
function SidebarTrigger({
  className,
  onClick,
  ...props
}: SidebarTriggerProps) {
  const { state, toggleSidebar } = useSidebar();
  // The design swaps to a distinct chevron-right glyph when collapsed rather
  // than rotating chevron-left — read directly off the two "Button/Icon"
  // instances in the Type=Full / Type=Mini examples (componentId 2185:777
  // "chevron-left" vs 2185:781 "chevron-right").
  const ChevronIcon = state === "collapsed" ? ChevronRight : ChevronLeft;

  return (
    <Button
      data-slot="sidebar-trigger"
      variant="outline"
      size="icon-xs"
      className={cn(
        "size-6 rounded-full bg-bg-surface-1 backdrop-blur-[40px] [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      onClick={(event) => {
        onClick?.(event);
        toggleSidebar();
      }}
      {...props}
    >
      <ChevronIcon />
      <span className="sr-only">Toggle Sidebar</span>
    </Button>
  );
}

export type SidebarRailProps = React.ComponentProps<"button">;

function SidebarRail({ className, ...props }: SidebarRailProps) {
  const { toggleSidebar } = useSidebar();

  return (
    <button
      data-slot="sidebar-rail"
      aria-label="Toggle Sidebar"
      tabIndex={-1}
      onClick={toggleSidebar}
      title="Toggle Sidebar"
      className={cn(
        "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]/sidebar:-right-4 group-data-[side=right]/sidebar:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border-hover sm:flex",
        "in-data-[side=left]:cursor-w-resize 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",
        "group-data-[collapsible=offcanvas]/sidebar:translate-x-0 group-data-[collapsible=offcanvas]/sidebar:after:left-full hover:group-data-[collapsible=offcanvas]/sidebar:bg-bg-surface-1",
        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarInsetProps = React.ComponentProps<"main">;

function SidebarInset({ className, ...props }: SidebarInsetProps) {
  return (
    <main
      data-slot="sidebar-inset"
      className={cn(
        "relative flex w-full flex-1 flex-col bg-bg-main 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]:peer-data-[state=collapsed]:ml-2",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarInputProps = React.ComponentProps<typeof Input>;

function SidebarInput({
  className,
  ...props
}: SidebarInputProps) {
  return (
    <Input
      data-slot="sidebar-input"
      className={cn("w-full bg-bg-main", className)}
      {...props}
    />
  );
}

export type SidebarHeaderProps = React.ComponentProps<"div">;

/** 80px tall slot for the product logo, divided from the body by a hairline. */
function SidebarHeader({ className, ...props }: SidebarHeaderProps) {
  return (
    <div
      data-slot="sidebar-header"
      className={cn(
        "relative flex h-26.5 shrink-0 items-center gap-2 border-b border-border-default px-4 py-6 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarFooterProps = React.ComponentProps<"div">;

/** The account slot: a wider 20px vertical rhythm than the rest of the panel. */
function SidebarFooter({ className, ...props }: SidebarFooterProps) {
  return (
    <div
      data-slot="sidebar-footer"
      className={cn(
        "flex shrink-0 flex-col gap-2 border-t border-border-default px-4 py-3 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarSeparatorProps = React.ComponentProps<typeof Separator>;

function SidebarSeparator({
  className,
  ...props
}: SidebarSeparatorProps) {
  return (
    <Separator
      data-slot="sidebar-separator"
      emphasis={"light"}
      className={cn("my-1 w-full", className)}
      {...props}
    />
  );
}

export type SidebarContentProps = React.ComponentProps<"div">;

function SidebarContent({ className, ...props }: SidebarContentProps) {
  return (
    <div
      data-slot="sidebar-content"
      className={cn(
        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto py-2 group-data-[collapsible=icon]/sidebar:overflow-hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupProps = React.ComponentProps<"div">;

/** Menu rows inset 16px; the mini rail centres its 40px squares at 20px. */
function SidebarGroup({ className, ...props }: SidebarGroupProps) {
  return (
    <div
      data-slot="sidebar-group"
      className={cn(
        "relative flex w-full min-w-0 flex-col px-4 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupLabelProps = React.ComponentProps<"div"> & { asChild?: boolean };

function SidebarGroupLabel({
  className,
  asChild = false,
  ...props
}: SidebarGroupLabelProps) {
  const Comp = asChild ? Slot.Root : "div";

  return (
    <Comp
      data-slot="sidebar-group-label"
      className={cn(
        // Sections sit 24px from the panel edge — 8px further out than the rows
        // they label, hence the negative inset against SidebarGroup's px-4.
        "flex shrink-0 items-center px-2 h-8 text-sm leading-6 font-medium tracking-[-0.2px] text-text-note ring-ring-neutral-shadow outline-none transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]/sidebar:pointer-events-none group-data-[collapsible=icon]/sidebar:-mt-8 group-data-[collapsible=icon]/sidebar:opacity-0 focus-visible:ring-3 [&>svg]:size-4 [&>svg]:shrink-0",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupActionProps = React.ComponentProps<"button"> & { asChild?: boolean };

function SidebarGroupAction({
  className,
  asChild = false,
  ...props
}: SidebarGroupActionProps) {
  const Comp = asChild ? Slot.Root : "button";

  return (
    <Comp
      data-slot="sidebar-group-action"
      className={cn(
        "absolute top-2.5 right-4 flex aspect-square w-6 items-center justify-center rounded-lg p-0 text-icon-muted ring-ring-neutral-shadow outline-none transition-transform group-data-[collapsible=icon]/sidebar:hidden after:absolute after:-inset-2 hover:bg-bg-surface-2 hover:text-icon-strong focus-visible:ring-3 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupContentProps = React.ComponentProps<"div">;

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

export type SidebarMenuProps = React.ComponentProps<"ul">;

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

export type SidebarMenuItemProps = React.ComponentProps<"li">;

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

const sidebarMenuButtonVariants = cva(
  // Hover and selected are the same 5% surface in the design — selection is
  // carried by the icon and the active sub-item rail, not by a second colour.
  //
  // The mini rail keeps only the row's leading child — icon, avatar, whatever
  // it is — and hides the rest. Hiding by element type instead would take out
  // any leading element that happens to be a span (Avatar's root is one).
  //
  // Focus draws a 1px ring-neutral-outline stroke plus a bg-main fill and a
  // ring-neutral-shadow ring in the design. The stroke is an inset ring, not a
  // border: a border would take 2px out of the 40px mini square, leaving 22px
  // for a 24px icon.
  "peer/menu-button group/menu-button flex w-full items-center gap-4 overflow-hidden rounded-lg p-2 text-left leading-[1.4] font-medium text-text-strong ring-ring-neutral-shadow outline-none transition-[width,height,padding] duration-200 ease-linear group-has-data-[slot=sidebar-menu-action]/menu-item:pr-10 group-has-data-[slot=sidebar-menu-badge]/menu-item:pr-12 group-data-[collapsible=icon]/sidebar:size-(--sidebar-item)! group-data-[collapsible=icon]/sidebar:p-2! group-data-[collapsible=icon]/sidebar:[&>*:not(:first-child)]:hidden hover:bg-blue-50 hover:text-blue-600 focus-visible:bg-bg-main focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:inset-ring focus-visible:inset-ring-ring-neutral-outline active:bg-blue-50 disabled:pointer-events-none disabled:text-text-disabled disabled:[&_svg]:text-icon-disabled aria-disabled:pointer-events-none aria-disabled:text-text-disabled data-open:hover:bg-blue-50 data-active:bg-blue-50 data-active:font-semibold data-active:text-blue-600 data-active:[&_svg]:text-blue-600 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6 [&>span:last-child]:truncate",
  {
    variants: {
      variant: {
        default: "",
        outline:
          "bg-bg-main shadow-[0_0_0_1px_var(--border-default)] hover:bg-blue-50 hover:shadow-[0_0_0_1px_var(--border-hover)]",
      },
      size: {
        default: "text-base",
        sm: "text-sm [&_svg:not([class*='size-'])]:size-5",
        // The mini square holds whatever leads the row: the 40px workspace logo
        // fills it (p-0), the design's 32px account avatar sits (40 - 32) / 2 =
        // 4px in (p-1). Centred by padding, never justify-center — padding
        // animates with the width, justify-center snaps before the rail shrinks.
        lg: "h-14 text-base group-data-[collapsible=icon]/sidebar:p-0! group-data-[collapsible=icon]/sidebar:has-[>[data-slot=avatar][data-size=sm]]:p-1!",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

export type SidebarMenuButtonProps = React.ComponentProps<"button"> & {
  asChild?: boolean;
  isActive?: boolean;
  tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>;

function SidebarMenuButton({
  asChild = false,
  isActive = false,
  variant = "default",
  size = "default",
  tooltip,
  className,
  ...props
}: SidebarMenuButtonProps) {
  const Comp = asChild ? Slot.Root : "button";
  const { isMobile, state } = useSidebar();

  const button = (
    <Comp
      data-slot="sidebar-menu-button"
      data-size={size}
      data-active={isActive}
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
      {...props}
    />
  );

  if (!tooltip) {
    return button;
  }

  if (typeof tooltip === "string") {
    tooltip = {
      children: tooltip,
    };
  }

  return (
    <Tooltip>
      <TooltipTrigger asChild>{button}</TooltipTrigger>
      <TooltipContent
        side="right"
        align="center"
        hidden={state !== "collapsed" || isMobile}
        {...tooltip}
      />
    </Tooltip>
  );
}

export type SidebarMenuActionProps = React.ComponentProps<"button"> & {
  asChild?: boolean;
  showOnHover?: boolean;
};

function SidebarMenuAction({
  className,
  asChild = false,
  showOnHover = false,
  ...props
}: SidebarMenuActionProps) {
  const Comp = asChild ? Slot.Root : "button";

  return (
    <Comp
      data-slot="sidebar-menu-action"
      className={cn(
        "absolute top-1/2 right-2 flex aspect-square w-6 -translate-y-1/2 items-center justify-center rounded-lg p-0 text-icon-muted ring-ring-neutral-shadow outline-none transition-transform group-data-[collapsible=icon]/sidebar:hidden peer-hover/menu-button:text-icon-strong after:absolute after:-inset-2 hover:bg-bg-surface-2 hover:text-icon-strong focus-visible:ring-3 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
        showOnHover &&
          "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-icon-strong aria-expanded:opacity-100 md:opacity-0",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarMenuBadgeProps = React.ComponentProps<"div">;

/** The counter pill the design parks at the trailing edge of a row. */
function SidebarMenuBadge({
  className,
  ...props
}: SidebarMenuBadgeProps) {
  return (
    <div
      data-slot="sidebar-menu-badge"
      className={cn(
        "pointer-events-none absolute top-1/2 right-2 flex min-w-5 -translate-y-1/2 items-center justify-center rounded-full bg-bg-surface-2 px-1 py-0.5 text-xs leading-4 font-medium text-text-strong tabular-nums select-none group-data-[collapsible=icon]/sidebar:hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarMenuSkeletonProps = React.ComponentProps<"div"> & {
  showIcon?: boolean;
};

function SidebarMenuSkeleton({
  className,
  showIcon = false,
  ...props
}: SidebarMenuSkeletonProps) {
  // Random width between 50 to 90%.
  const [width] = React.useState(() => {
    return `${Math.floor(Math.random() * 40) + 50}%`;
  });

  return (
    <div
      data-slot="sidebar-menu-skeleton"
      className={cn("flex h-10 items-center gap-4 rounded-xl p-2", className)}
      {...props}
    >
      {showIcon && (
        <Skeleton
          data-slot="sidebar-menu-skeleton-icon"
          className="size-6 shrink-0 rounded-md bg-bg-surface-2"
        />
      )}
      <Skeleton
        data-slot="sidebar-menu-skeleton-text"
        className="h-4 max-w-(--skeleton-width) flex-1 rounded-md bg-bg-surface-2"
        style={
          {
            "--skeleton-width": width,
          } as React.CSSProperties
        }
      />
    </div>
  );
}

export type SidebarMenuSubProps = React.ComponentProps<"ul">;

function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
  return (
    <ul
      data-slot="sidebar-menu-sub"
      className={cn(
        "flex min-w-0 flex-col px-2 py-1 group-data-[collapsible=icon]/sidebar:hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarMenuSubItemProps = React.ComponentProps<"li">;

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

export type SidebarMenuSubButtonProps = React.ComponentProps<"a"> & {
  asChild?: boolean;
  size?: "sm" | "md";
  isActive?: boolean;
};

function SidebarMenuSubButton({
  asChild = false,
  size = "md",
  isActive = false,
  className,
  ...props
}: SidebarMenuSubButtonProps) {
  const Comp = asChild ? Slot.Root : "a";

  return (
    <Comp
      data-slot="sidebar-menu-sub-button"
      data-size={size}
      data-active={isActive}
      className={cn(
        // The leading indicator is a 6px dot ("Bullet" in the design) rendered
        // as a pseudo-element, not a real flex child — so the label's own
        // indent has to reserve the room for it: 18px of panel to the dot,
        // its own 6px, then the 16px gap to the label. That reserved total is
        // pl-10 (40px); the dot itself is positioned independently via
        // `before:left`, which ignores padding.
        //
        // Focus draws the same border-transparent → border-ring-neutral-outline
        // reveal as the top-level menu button (rule 11): reserved at rest so
        // the row's h=40 never shifts.
        "relative flex h-10 min-w-0 items-center gap-4 overflow-hidden rounded-xl border border-transparent py-2 pr-4 pl-10 leading-[1.4] font-medium text-text-muted ring-ring-neutral-shadow outline-none",
        "before:absolute before:top-1/2 before:left-[18px] before:size-1.5 before:-translate-y-1/2 before:rounded-full before:bg-sky-200",
        "hover:bg-bg-surface-1 focus-visible:border-ring-neutral-outline focus-visible:bg-bg-main focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow active:bg-bg-surface-1",
        "aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:before:bg-text-disabled",
        "data-[size=md]:text-base data-[size=sm]:text-sm",
        "data-active:font-semibold data-active:text-text-strong data-active:before:bg-blue-500",
        "[&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0",
        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,
  sidebarMenuButtonVariants,
  useSidebar,
};
components/ui/sidebar.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronLeft } from "@/components/icons/chevron-left";
import { ChevronRight } from "@/components/icons/chevron-right";

import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
/** Full and mini rail widths are drawn at 292px / 80px in the design. */
const SIDEBAR_WIDTH = "292px";
const SIDEBAR_WIDTH_MOBILE = "292px";
const SIDEBAR_WIDTH_ICON = "80px";
/**
 * The mini rail's square: every collapsed row, logo and avatar slot is this
 * size, centred by an equal gutter on each side. Keep the two in one equation
 * — changing the rail or the square re-derives the gutter for every slot.
 */
const SIDEBAR_ITEM = "40px";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";

export type SidebarContextProps = {
  state: "expanded" | "collapsed";
  open: boolean;
  setOpen: (open: boolean) => void;
  openMobile: boolean;
  setOpenMobile: (open: boolean) => void;
  isMobile: 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;
}

export type SidebarProviderProps = React.ComponentProps<"div"> & {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
};

function SidebarProvider({
  defaultOpen = true,
  open: openProp,
  onOpenChange: setOpenProp,
  className,
  style,
  children,
  ...props
}: SidebarProviderProps) {
  const isMobile = useIsMobile();
  const [openMobile, setOpenMobile] = React.useState(false);

  const [_open, _setOpen] = React.useState(defaultOpen);
  const open = openProp ?? _open;
  const setOpen = React.useCallback(
    (value: boolean | ((value: boolean) => boolean)) => {
      const openState = typeof value === "function" ? value(open) : value;
      if (setOpenProp) {
        setOpenProp(openState);
      } else {
        _setOpen(openState);
      }

      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
    },
    [setOpenProp, open],
  );

  const toggleSidebar = React.useCallback(() => {
    return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
  }, [isMobile, setOpen, setOpenMobile]);

  React.useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (
        event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
        (event.metaKey || event.ctrlKey)
      ) {
        event.preventDefault();
        toggleSidebar();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [toggleSidebar]);

  const state = open ? "expanded" : "collapsed";

  const contextValue = React.useMemo<SidebarContextProps>(
    () => ({
      state,
      open,
      setOpen,
      isMobile,
      openMobile,
      setOpenMobile,
      toggleSidebar,
    }),
    [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
  );

  return (
    <SidebarContext value={contextValue}>
      <TooltipProvider delay={0}>
        <div
          data-slot="sidebar-wrapper"
          style={
            {
              "--sidebar-width": SIDEBAR_WIDTH,
              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
              "--sidebar-item": SIDEBAR_ITEM,
              "--sidebar-gutter":
                "calc((var(--sidebar-width-icon) - var(--sidebar-item)) / 2)",
              ...style,
            } as React.CSSProperties
          }
          className={cn(
            "group/sidebar-wrapper flex min-h-svh w-full bg-bg-main",
            className,
          )}
          {...props}
        >
          {children}
        </div>
      </TooltipProvider>
    </SidebarContext>
  );
}

/** The panel body — the surface every layout variant renders its children into. */
const SIDEBAR_PANEL = "flex size-full flex-col bg-bg-main text-text-strong";

export type SidebarProps = React.ComponentProps<"div"> & {
  side?: "left" | "right";
  variant?: "sidebar" | "floating" | "inset";
  collapsible?: "offcanvas" | "icon" | "none";
};

function Sidebar({
  side = "left",
  variant = "sidebar",
  collapsible = "offcanvas",
  className,
  children,
  ...props
}: SidebarProps) {
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();

  if (collapsible === "none") {
    return (
      <div
        data-slot="sidebar"
        data-state="expanded"
        className={cn(
          "group/sidebar w-(--sidebar-width)",
          SIDEBAR_PANEL,
          className,
        )}
        {...props}
      >
        {children}
      </div>
    );
  }

  if (isMobile) {
    return (
      <DialogPrimitive.Root open={openMobile} onOpenChange={setOpenMobile}>
        <DialogPrimitive.Portal>
          <DialogPrimitive.Backdrop
            data-slot="sidebar-overlay"
            className="fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
          />
          <DialogPrimitive.Popup
            data-slot="sidebar"
            data-mobile="true"
            data-side={side}
            data-state={state}
            className={cn(
              "group/sidebar fixed inset-y-0 z-50 w-(--sidebar-width) outline-none duration-200 data-open:animate-in data-closed:animate-out",
              side === "left"
                ? "left-0 data-open:slide-in-from-left data-closed:slide-out-to-left"
                : "right-0 data-open:slide-in-from-right data-closed:slide-out-to-right",
              className,
            )}
            style={
              {
                "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
              } as React.CSSProperties
            }
            {...props}
          >
            <DialogPrimitive.Title className="sr-only">
              Sidebar
            </DialogPrimitive.Title>
            <DialogPrimitive.Description className="sr-only">
              Displays the mobile sidebar.
            </DialogPrimitive.Description>
            <div className={SIDEBAR_PANEL}>{children}</div>
          </DialogPrimitive.Popup>
        </DialogPrimitive.Portal>
      </DialogPrimitive.Root>
    );
  }

  return (
    <div
      className="group/sidebar peer hidden text-text-strong md:block"
      data-state={state}
      data-collapsible={state === "collapsed" ? collapsible : ""}
      data-variant={variant}
      data-side={side}
      data-slot="sidebar"
    >
      {/* Holds the space the fixed panel occupies in the page flow. */}
      <div
        data-slot="sidebar-gap"
        className={cn(
          "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
          "group-data-[collapsible=offcanvas]/sidebar:w-0",
          "group-data-[side=right]/sidebar:rotate-180",
          variant === "floating" || variant === "inset"
            ? "group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
            : "group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon)",
        )}
      />
      <div
        data-slot="sidebar-container"
        data-side={side}
        className={cn(
          "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]/sidebar:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]/sidebar:right-[calc(var(--sidebar-width)*-1)] md:flex",
          variant === "floating" || variant === "inset"
            ? "p-2 group-data-[collapsible=icon]/sidebar:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
            : // The design's stroke on the panel has no per-side override, so
              // it runs on all four edges, not just the trailing one.
              "group-data-[collapsible=icon]/sidebar:w-(--sidebar-width-icon) border border-border-default",
          className,
        )}
        {...props}
      >
        <div
          data-slot="sidebar-inner"
          className={cn(
            SIDEBAR_PANEL,
            "group-data-[variant=floating]/sidebar:rounded-xl group-data-[variant=floating]/sidebar:border group-data-[variant=floating]/sidebar:border-border-default",
          )}
        >
          {children}
        </div>
      </div>
    </div>
  );
}

export type SidebarTriggerProps = React.ComponentProps<typeof Button>;

/**
 * The 24px collapse pill. The design pins it half-outside the panel's trailing
 * edge, which is a placement decision for the layout — pass it as `className`.
 *
 * Position it with insets or margins, never a transform: Button presses with
 * `active:translate-y-px`, and that active utility replaces the whole
 * `translate` property, so a centring `-translate-y-1/2` would drop out on
 * mousedown and the pill would jump by half its height.
 */
function SidebarTrigger({
  className,
  onClick,
  ...props
}: SidebarTriggerProps) {
  const { state, toggleSidebar } = useSidebar();
  // The design swaps to a distinct chevron-right glyph when collapsed rather
  // than rotating chevron-left — read directly off the two "Button/Icon"
  // instances in the Type=Full / Type=Mini examples (componentId 2185:777
  // "chevron-left" vs 2185:781 "chevron-right").
  const ChevronIcon = state === "collapsed" ? ChevronRight : ChevronLeft;

  return (
    <Button
      data-slot="sidebar-trigger"
      variant="outline"
      size="icon-xs"
      className={cn(
        "size-6 rounded-full bg-bg-surface-1 backdrop-blur-[40px] [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      onClick={(event) => {
        onClick?.(event);
        toggleSidebar();
      }}
      {...props}
    >
      <ChevronIcon />
      <span className="sr-only">Toggle Sidebar</span>
    </Button>
  );
}

export type SidebarRailProps = React.ComponentProps<"button">;

function SidebarRail({ className, ...props }: SidebarRailProps) {
  const { toggleSidebar } = useSidebar();

  return (
    <button
      data-slot="sidebar-rail"
      aria-label="Toggle Sidebar"
      tabIndex={-1}
      onClick={toggleSidebar}
      title="Toggle Sidebar"
      className={cn(
        "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]/sidebar:-right-4 group-data-[side=right]/sidebar:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border-hover sm:flex",
        "in-data-[side=left]:cursor-w-resize 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",
        "group-data-[collapsible=offcanvas]/sidebar:translate-x-0 group-data-[collapsible=offcanvas]/sidebar:after:left-full hover:group-data-[collapsible=offcanvas]/sidebar:bg-bg-surface-1",
        "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
        "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarInsetProps = React.ComponentProps<"main">;

function SidebarInset({ className, ...props }: SidebarInsetProps) {
  return (
    <main
      data-slot="sidebar-inset"
      className={cn(
        "relative flex w-full flex-1 flex-col bg-bg-main 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]:peer-data-[state=collapsed]:ml-2",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarInputProps = React.ComponentProps<typeof Input>;

function SidebarInput({
  className,
  ...props
}: SidebarInputProps) {
  return (
    <Input
      data-slot="sidebar-input"
      className={cn("w-full bg-bg-main", className)}
      {...props}
    />
  );
}

export type SidebarHeaderProps = React.ComponentProps<"div">;

/** 80px tall slot for the product logo, divided from the body by a hairline. */
function SidebarHeader({ className, ...props }: SidebarHeaderProps) {
  return (
    <div
      data-slot="sidebar-header"
      className={cn(
        "relative flex h-26.5 shrink-0 items-center gap-2 border-b border-border-default px-4 py-6 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarFooterProps = React.ComponentProps<"div">;

/** The account slot: a wider 20px vertical rhythm than the rest of the panel. */
function SidebarFooter({ className, ...props }: SidebarFooterProps) {
  return (
    <div
      data-slot="sidebar-footer"
      className={cn(
        "flex shrink-0 flex-col gap-2 border-t border-border-default px-4 py-3 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarSeparatorProps = React.ComponentProps<typeof Separator>;

function SidebarSeparator({
  className,
  ...props
}: SidebarSeparatorProps) {
  return (
    <Separator
      data-slot="sidebar-separator"
      emphasis={"light"}
      className={cn("my-1 w-full", className)}
      {...props}
    />
  );
}

export type SidebarContentProps = React.ComponentProps<"div">;

function SidebarContent({ className, ...props }: SidebarContentProps) {
  return (
    <div
      data-slot="sidebar-content"
      className={cn(
        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto py-2 group-data-[collapsible=icon]/sidebar:overflow-hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupProps = React.ComponentProps<"div">;

/** Menu rows inset 16px; the mini rail centres its 40px squares at 20px. */
function SidebarGroup({ className, ...props }: SidebarGroupProps) {
  return (
    <div
      data-slot="sidebar-group"
      className={cn(
        "relative flex w-full min-w-0 flex-col px-4 group-data-[collapsible=icon]/sidebar:px-(--sidebar-gutter) transition-[padding] duration-200 ease-linear",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarGroupLabelProps = useRender.ComponentProps<"div"> & React.ComponentProps<"div">;

function SidebarGroupLabel({
  className,
  render,
  ...props
}: SidebarGroupLabelProps) {
  return useRender({
    defaultTagName: "div",
    props: mergeProps<"div">(
      {
        className: cn(
          // Sections sit 24px from the panel edge — 8px further out than the rows
          // they label, hence the negative inset against SidebarGroup's px-4.
          "flex shrink-0 items-center px-2 h-8 text-sm leading-6 font-medium tracking-[-0.2px] text-text-note ring-ring-neutral-shadow outline-none transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]/sidebar:pointer-events-none group-data-[collapsible=icon]/sidebar:-mt-8 group-data-[collapsible=icon]/sidebar:opacity-0 focus-visible:ring-3 [&>svg]:size-4 [&>svg]:shrink-0",
          className,
        ),
      },
      props,
    ),
    render,
    state: {
      slot: "sidebar-group-label",
    },
  });
}

export type SidebarGroupActionProps = useRender.ComponentProps<"button"> & React.ComponentProps<"button">;

function SidebarGroupAction({
  className,
  render,
  ...props
}: SidebarGroupActionProps) {
  return useRender({
    defaultTagName: "button",
    props: mergeProps<"button">(
      {
        className: cn(
          "absolute top-2.5 right-4 flex aspect-square w-6 items-center justify-center rounded-lg p-0 text-icon-muted ring-ring-neutral-shadow outline-none transition-transform group-data-[collapsible=icon]/sidebar:hidden after:absolute after:-inset-2 hover:bg-bg-surface-2 hover:text-icon-strong focus-visible:ring-3 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
          className,
        ),
      },
      props,
    ),
    render,
    state: {
      slot: "sidebar-group-action",
    },
  });
}

export type SidebarGroupContentProps = React.ComponentProps<"div">;

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

export type SidebarMenuProps = React.ComponentProps<"ul">;

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

export type SidebarMenuItemProps = React.ComponentProps<"li">;

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

const sidebarMenuButtonVariants = cva(
  // Hover and selected are the same 5% surface in the design — selection is
  // carried by the icon and the active sub-item rail, not by a second colour.
  //
  // The mini rail keeps only the row's leading child — icon, avatar, whatever
  // it is — and hides the rest. Hiding by element type instead would take out
  // any leading element that happens to be a span (Avatar's root is one).
  //
  // Focus draws a 1px ring-neutral-outline stroke plus a bg-main fill and a
  // ring-neutral-shadow ring in the design. The stroke is an inset ring, not a
  // border: a border would take 2px out of the 40px mini square, leaving 22px
  // for a 24px icon.
  //
  // Composed as a menu trigger, the base flavour marks the open row with
  // data-popup-open (Radix used data-state="open"), so the hover surface below
  // carries both spellings and the row keeps its fill while a menu is open on
  // it.
  "peer/menu-button group/menu-button flex w-full items-center gap-4 overflow-hidden rounded-lg p-2 text-left leading-[1.4] font-medium text-text-strong ring-ring-neutral-shadow outline-none transition-[width,height,padding] duration-200 ease-linear group-has-data-[slot=sidebar-menu-action]/menu-item:pr-10 group-has-data-[slot=sidebar-menu-badge]/menu-item:pr-12 group-data-[collapsible=icon]/sidebar:size-(--sidebar-item)! group-data-[collapsible=icon]/sidebar:p-2! group-data-[collapsible=icon]/sidebar:[&>*:not(:first-child)]:hidden hover:bg-blue-50 hover:text-blue-600 focus-visible:bg-bg-main focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:inset-ring focus-visible:inset-ring-ring-neutral-outline active:bg-blue-50 disabled:pointer-events-none disabled:text-text-disabled disabled:[&_svg]:text-icon-disabled aria-disabled:pointer-events-none aria-disabled:text-text-disabled data-open:hover:bg-blue-50 data-popup-open:hover:bg-bg-surface-1 data-active:bg-blue-50 data-active:font-semibold data-active:text-blue-600 data-active:[&_svg]:text-blue-600 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6 [&>span:last-child]:truncate",
  {
    variants: {
      variant: {
        default: "",
        outline:
          "bg-bg-main shadow-[0_0_0_1px_var(--border-default)] hover:bg-blue-50 hover:shadow-[0_0_0_1px_var(--border-hover)]",
      },
      size: {
        default: "text-base",
        sm: "text-sm [&_svg:not([class*='size-'])]:size-5",
        // The mini square holds whatever leads the row: the 40px workspace logo
        // fills it (p-0), the design's 32px account avatar sits (40 - 32) / 2 =
        // 4px in (p-1). Centred by padding, never justify-center — padding
        // animates with the width, justify-center snaps before the rail shrinks.
        lg: "h-14 text-base group-data-[collapsible=icon]/sidebar:p-0! group-data-[collapsible=icon]/sidebar:has-[>[data-slot=avatar][data-size=sm]]:p-1!",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

export type SidebarMenuButtonProps = useRender.ComponentProps<"button"> &
  React.ComponentProps<"button"> & {
    isActive?: boolean;
    tooltip?: string | React.ComponentProps<typeof TooltipContent>;
  } & VariantProps<typeof sidebarMenuButtonVariants>;

function SidebarMenuButton({
  render,
  isActive = false,
  variant = "default",
  size = "default",
  tooltip,
  className,
  ...props
}: SidebarMenuButtonProps) {
  const { isMobile, state } = useSidebar();

  const button = useRender({
    defaultTagName: "button",
    props: mergeProps<"button">(
      {
        className: cn(sidebarMenuButtonVariants({ variant, size }), className),
      },
      props,
    ),
    // Tooltip.Trigger takes `disabled` for itself (it only mutes the tooltip), so the button it
    // renders is given the disabled state directly; Radix's Slot forwards it to the button.
    render: !tooltip ? (
      render
    ) : (
      <TooltipTrigger render={render ?? <button disabled={props.disabled} />} />
    ),
    state: {
      slot: "sidebar-menu-button",
      size,
      active: isActive,
    },
  });

  if (!tooltip) {
    return button;
  }

  if (typeof tooltip === "string") {
    tooltip = {
      children: tooltip,
    };
  }

  return (
    <Tooltip>
      {button}
      <TooltipContent
        side="right"
        align="center"
        hidden={state !== "collapsed" || isMobile}
        {...tooltip}
      />
    </Tooltip>
  );
}

export type SidebarMenuActionProps = useRender.ComponentProps<"button"> &
  React.ComponentProps<"button"> & {
    showOnHover?: boolean;
  };

function SidebarMenuAction({
  className,
  render,
  showOnHover = false,
  ...props
}: SidebarMenuActionProps) {
  return useRender({
    defaultTagName: "button",
    props: mergeProps<"button">(
      {
        className: cn(
          "absolute top-1/2 right-2 flex aspect-square w-6 -translate-y-1/2 items-center justify-center rounded-lg p-0 text-icon-muted ring-ring-neutral-shadow outline-none transition-transform group-data-[collapsible=icon]/sidebar:hidden peer-hover/menu-button:text-icon-strong after:absolute after:-inset-2 hover:bg-bg-surface-2 hover:text-icon-strong focus-visible:ring-3 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
          showOnHover &&
            "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-icon-strong aria-expanded:opacity-100 md:opacity-0",
          className,
        ),
      },
      props,
    ),
    render,
    state: {
      slot: "sidebar-menu-action",
    },
  });
}

export type SidebarMenuBadgeProps = React.ComponentProps<"div">;

/** The counter pill the design parks at the trailing edge of a row. */
function SidebarMenuBadge({
  className,
  ...props
}: SidebarMenuBadgeProps) {
  return (
    <div
      data-slot="sidebar-menu-badge"
      className={cn(
        "pointer-events-none absolute top-1/2 right-2 flex min-w-5 -translate-y-1/2 items-center justify-center rounded-full bg-bg-surface-2 px-1 py-0.5 text-xs leading-4 font-medium text-text-strong tabular-nums select-none group-data-[collapsible=icon]/sidebar:hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarMenuSkeletonProps = React.ComponentProps<"div"> & {
  showIcon?: boolean;
};

function SidebarMenuSkeleton({
  className,
  showIcon = false,
  ...props
}: SidebarMenuSkeletonProps) {
  // Random width between 50 to 90%.
  const [width] = React.useState(() => {
    return `${Math.floor(Math.random() * 40) + 50}%`;
  });

  return (
    <div
      data-slot="sidebar-menu-skeleton"
      className={cn("flex h-10 items-center gap-4 rounded-xl p-2", className)}
      {...props}
    >
      {showIcon && (
        <Skeleton
          data-slot="sidebar-menu-skeleton-icon"
          className="size-6 shrink-0 rounded-md bg-bg-surface-2"
        />
      )}
      <Skeleton
        data-slot="sidebar-menu-skeleton-text"
        className="h-4 max-w-(--skeleton-width) flex-1 rounded-md bg-bg-surface-2"
        style={
          {
            "--skeleton-width": width,
          } as React.CSSProperties
        }
      />
    </div>
  );
}

export type SidebarMenuSubProps = React.ComponentProps<"ul">;

function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
  return (
    <ul
      data-slot="sidebar-menu-sub"
      className={cn(
        "flex min-w-0 flex-col px-2 py-1 group-data-[collapsible=icon]/sidebar:hidden",
        className,
      )}
      {...props}
    />
  );
}

export type SidebarMenuSubItemProps = React.ComponentProps<"li">;

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

export type SidebarMenuSubButtonProps = useRender.ComponentProps<"a"> &
  React.ComponentProps<"a"> & {
    size?: "sm" | "md";
    isActive?: boolean;
  };

function SidebarMenuSubButton({
  render,
  size = "md",
  isActive = false,
  className,
  ...props
}: SidebarMenuSubButtonProps) {
  return useRender({
    defaultTagName: "a",
    props: mergeProps<"a">(
      {
        className: cn(
          // The leading indicator is a 6px dot ("Bullet" in the design) rendered
          // as a pseudo-element, not a real flex child — so the label's own
          // indent has to reserve the room for it: 18px of panel to the dot,
          // its own 6px, then the 16px gap to the label. That reserved total is
          // pl-10 (40px); the dot itself is positioned independently via
          // `before:left`, which ignores padding.
          //
          // Focus draws the same border-transparent → border-ring-neutral-outline
          // reveal as the top-level menu button (rule 11): reserved at rest so
          // the row's h=40 never shifts.
          "relative flex h-10 min-w-0 items-center gap-4 overflow-hidden rounded-xl border border-transparent py-2 pr-4 pl-10 leading-[1.4] font-medium text-text-muted ring-ring-neutral-shadow outline-none",
          "before:absolute before:top-1/2 before:left-[18px] before:size-1.5 before:-translate-y-1/2 before:rounded-full before:bg-sky-200",
          "hover:bg-bg-surface-1 focus-visible:border-ring-neutral-outline focus-visible:bg-bg-main focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow active:bg-bg-surface-1",
          "aria-disabled:pointer-events-none aria-disabled:text-text-disabled aria-disabled:before:bg-text-disabled",
          "data-[size=md]:text-base data-[size=sm]:text-sm",
          "data-active:font-semibold data-active:text-text-strong data-active:before:bg-blue-500",
          "[&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0",
          className,
        ),
      },
      props,
    ),
    render,
    state: {
      slot: "sidebar-menu-sub-button",
      size,
      active: isActive,
    },
  });
}

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