{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-shell-1",
  "title": "App Shell 1: collapsible sidebar with command menu",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A product frame: workspace switcher, grouped navigation with badges that collapses to an icon rail, top bar with search, notifications and a ⌘K command menu, and a scrolling page slot. A drawer on phones.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/command.json",
    "https://ui.ballmac.com/r/dropdown-menu.json",
    "https://ui.ballmac.com/r/kbd.json",
    "https://ui.ballmac.com/r/sheet.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/app-shell-1/app-shell-1.tsx",
      "content": "// Ballmac UI: App Shell 1. https://ui.ballmac.com/blocks/app-shell-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Bell, ChevronsUpDown, FolderKanban, Home, Inbox, LayoutDashboard, PanelLeft, Search, Settings, Sparkles, Users } from \"lucide-react\"\n\nimport { Avatar, AvatarFallback } from \"@/components/ballmac/avatar\"\nimport { Button, buttonVariants } from \"@/components/ballmac/button\"\nimport { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut } from \"@/components/ballmac/command\"\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from \"@/components/ballmac/dropdown-menu\"\nimport { Kbd } from \"@/components/ballmac/kbd\"\nimport { Sheet, SheetContent, SheetDescription, SheetTitle } from \"@/components/ballmac/sheet\"\nimport { cn } from \"@/lib/utils\"\n\ntype AppShell1Item = {\n  id: string\n  label: string\n  icon?: React.ReactNode\n  /** Small count or label at the end, e.g. unread items. */\n  badge?: string\n}\n\ntype AppShell1Group = {\n  /** Small heading above the group. */\n  label?: string\n  items: AppShell1Item[]\n}\n\ntype AppShell1Props = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Workspace name in the switcher. */\n  workspace?: string\n  /** Other workspaces offered in the switcher menu. */\n  workspaces?: string[]\n  /** Navigation groups. */\n  nav?: AppShell1Group[]\n  /** The signed-in person. */\n  user?: { name: string; email: string }\n  /** Controlled id of the open page. */\n  value?: string\n  /** Id of the open page when uncontrolled. */\n  defaultValue?: string\n  /** Called when another page is picked from the sidebar or the command menu. */\n  onValueChange?: (id: string) => void\n  /** Whether the sidebar starts collapsed to icons. */\n  defaultCollapsed?: boolean\n  /** Height of the frame. Use \"100svh\" for a full-screen app. */\n  height?: string\n  /** The page. Receives the title of the open item. Defaults to a sample page. */\n  children?: (page: AppShell1Item) => React.ReactNode\n}\n\nconst defaultNav: AppShell1Group[] = [\n  {\n    items: [\n      { id: \"overview\", label: \"Overview\", icon: <Home /> },\n      { id: \"inbox\", label: \"Inbox\", icon: <Inbox />, badge: \"4\" },\n      { id: \"projects\", label: \"Projects\", icon: <FolderKanban /> },\n      { id: \"reports\", label: \"Reports\", icon: <LayoutDashboard /> },\n    ],\n  },\n  {\n    label: \"Workspace\",\n    items: [\n      { id: \"team\", label: \"Team\", icon: <Users /> },\n      { id: \"settings\", label: \"Settings\", icon: <Settings /> },\n    ],\n  },\n]\n\nfunction SamplePage({ page }: { page: AppShell1Item }) {\n  return (\n    <div className=\"mx-auto max-w-4xl\">\n      <h1 className=\"text-2xl font-semibold tracking-[-0.03em]\">{page.label}</h1>\n      <p className=\"text-muted-foreground mt-1 text-sm\">Your page goes here. The sidebar, top bar and command menu stay put around it.</p>\n      <div className=\"mt-6 grid gap-4 sm:grid-cols-3\">\n        {[\n          [\"Open tasks\", \"24\"],\n          [\"Shipped this week\", \"9\"],\n          [\"Due soon\", \"6\"],\n        ].map(([label, value]) => (\n          <div key={label} className=\"bg-card rounded-2xl border p-4\">\n            <p className=\"text-muted-foreground text-xs\">{label}</p>\n            <p className=\"mt-2 text-3xl font-semibold tracking-tight tabular-nums\">{value}</p>\n          </div>\n        ))}\n      </div>\n      <div className=\"bg-card mt-4 rounded-2xl border p-5\">\n        <p className=\"text-sm font-medium\">Recent activity</p>\n        <ul className=\"divide-y\">\n          {[\"Ana closed 3 tasks in Launch plan\", \"Kofi commented on Pricing page\", \"Mei joined the workspace\"].map((t) => (\n            <li key={t} className=\"text-muted-foreground py-3 text-sm first:pt-3 last:pb-0\">{t}</li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  )\n}\n\nfunction SidebarBody({\n  nav,\n  value,\n  onPick,\n  collapsed,\n  workspace,\n  workspaces,\n  user,\n}: {\n  nav: AppShell1Group[]\n  value: string\n  onPick: (id: string) => void\n  collapsed: boolean\n  workspace: string\n  workspaces: string[]\n  user: { name: string; email: string }\n}) {\n  const initials = user.name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"p-3\">\n        <DropdownMenu>\n          <DropdownMenuTrigger\n            aria-label={`Workspace: ${workspace}`}\n            className=\"hover:bg-accent focus-visible:ring-ring/50 flex w-full items-center gap-2.5 rounded-xl p-1.5 text-start outline-none focus-visible:ring-[3px]\"\n          >\n            <span aria-hidden=\"true\" className=\"bg-foreground text-background flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold\">{workspace[0]}</span>\n            {!collapsed && (\n              <>\n                <span className=\"min-w-0 flex-1 truncate text-sm font-semibold\">{workspace}</span>\n                <ChevronsUpDown className=\"text-muted-foreground size-4 shrink-0\" aria-hidden=\"true\" />\n              </>\n            )}\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\" className=\"w-56\">\n            <DropdownMenuLabel>Switch workspace</DropdownMenuLabel>\n            {workspaces.map((w) => (\n              <DropdownMenuItem key={w}>{w}</DropdownMenuItem>\n            ))}\n            <DropdownMenuSeparator />\n            <DropdownMenuItem>Create workspace</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <nav aria-label=\"Main\" className=\"min-h-0 flex-1 space-y-5 overflow-y-auto px-3 pb-3\">\n        {nav.map((g, gi) => (\n          <div key={g.label ?? gi}>\n            {g.label && !collapsed && <p className=\"text-muted-foreground px-2.5 pb-1.5 text-xs font-medium\">{g.label}</p>}\n            <ul className=\"space-y-0.5\">\n              {g.items.map((item) => {\n                const active = item.id === value\n                return (\n                  <li key={item.id}>\n                    <a\n                      href={`#${item.id}`}\n                      aria-current={active ? \"page\" : undefined}\n                      aria-label={collapsed ? item.label : undefined}\n                      title={collapsed ? item.label : undefined}\n                      onClick={(e) => {\n                        e.preventDefault()\n                        onPick(item.id)\n                      }}\n                      className={cn(\n                        \"focus-visible:ring-ring/50 relative flex h-9 items-center gap-2.5 rounded-lg px-2.5 text-sm font-medium outline-none transition-colors focus-visible:ring-[3px] [&_svg]:size-4 [&_svg]:shrink-0\",\n                        active ? \"bg-accent text-accent-foreground\" : \"text-muted-foreground hover:bg-accent/60 hover:text-foreground\",\n                        collapsed && \"justify-center px-0\"\n                      )}\n                    >\n                      <span aria-hidden=\"true\">{item.icon}</span>\n                      {!collapsed && <span className=\"flex-1 truncate\">{item.label}</span>}\n                      {item.badge && !collapsed && <span className=\"bg-foreground text-background rounded-full px-1.5 py-px text-[11px] font-semibold tabular-nums\">{item.badge}</span>}\n                      {item.badge && collapsed && <span aria-hidden=\"true\" className=\"bg-foreground ring-background absolute top-1.5 end-2.5 size-2 rounded-full ring-2\" />}\n                    </a>\n                  </li>\n                )\n              })}\n            </ul>\n          </div>\n        ))}\n      </nav>\n\n      {!collapsed && (\n        <div className=\"px-3 pb-3\">\n          <div className=\"bg-muted/60 rounded-xl border p-3.5\">\n            <p className=\"flex items-center gap-1.5 text-sm font-semibold\"><Sparkles className=\"size-4\" aria-hidden=\"true\" />Try Pro free</p>\n            <p className=\"text-muted-foreground mt-1 text-xs leading-relaxed\">Unlimited projects and advanced reports for 14 days.</p>\n            <a href=\"#upgrade\" className={buttonVariants({ size: \"sm\", className: \"mt-3 w-full\" })}>Start trial</a>\n          </div>\n        </div>\n      )}\n\n      <div className=\"border-t p-3\">\n        <div className={cn(\"flex items-center gap-2.5 rounded-xl p-1.5\", collapsed && \"justify-center\")}>\n          <Avatar size=\"sm\"><AvatarFallback className=\"bg-chart-1/25 text-foreground text-xs font-semibold\">{initials}</AvatarFallback></Avatar>\n          {!collapsed && (\n            <div className=\"min-w-0 leading-tight\">\n              <p className=\"truncate text-sm font-medium\">{user.name}</p>\n              <p className=\"text-muted-foreground truncate text-xs\">{user.email}</p>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction AppShell1({\n  workspace = \"Acme\",\n  workspaces = [\"Acme\", \"Northwind Studio\", \"Personal\"],\n  nav = defaultNav,\n  user = { name: \"Jordan Lee\", email: \"jordan@acme.com\" },\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  defaultCollapsed = false,\n  height = \"44rem\",\n  children,\n  className,\n  style,\n  ...props\n}: AppShell1Props) {\n  const all = nav.flatMap((g) => g.items)\n  const [internal, setInternal] = React.useState(defaultValue ?? all[0]?.id ?? \"\")\n  const value = valueProp ?? internal\n  const [collapsed, setCollapsed] = React.useState(defaultCollapsed)\n  const [drawer, setDrawer] = React.useState(false)\n  const [palette, setPalette] = React.useState(false)\n  const page = all.find((i) => i.id === value) ?? all[0]\n  const mainId = React.useId()\n\n  const pick = React.useCallback(\n    (id: string) => {\n      if (valueProp === undefined) setInternal(id)\n      onValueChange?.(id)\n      setDrawer(false)\n      setPalette(false)\n    },\n    [onValueChange, valueProp]\n  )\n\n  // ⌘B / Ctrl+B collapses the sidebar while focus is inside the frame. ⌘K / Ctrl+K is handled by the command menu itself.\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  React.useEffect(() => {\n    const onKey = (e: KeyboardEvent) => {\n      if (!(e.metaKey || e.ctrlKey) || !rootRef.current?.contains(document.activeElement)) return\n      if (e.key.toLowerCase() === \"b\") {\n        e.preventDefault()\n        setCollapsed((c) => !c)\n      }\n    }\n    document.addEventListener(\"keydown\", onKey)\n    return () => document.removeEventListener(\"keydown\", onKey)\n  }, [])\n\n  if (!page) return null\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"app-shell-1\"\n      className={cn(\"bg-background @container relative flex overflow-hidden rounded-2xl border shadow-[0_30px_80px_-50px_rgb(0_0_0/0.4)]\", className)}\n      style={{ height, ...style }}\n      {...props}\n    >\n      <a href={`#${mainId}`} className=\"bg-background focus-visible:ring-ring/50 sr-only z-50 rounded-md px-3 py-2 text-sm focus:not-sr-only focus:absolute focus:top-2 focus:start-2 focus-visible:ring-[3px]\">Skip to content</a>\n\n      <aside\n        aria-label=\"Sidebar\"\n        className={cn(\"bg-muted/30 hidden shrink-0 border-e transition-[width] duration-200 ease-out motion-reduce:transition-none md:block\", collapsed ? \"w-[4.25rem]\" : \"w-60\")}\n      >\n        <SidebarBody nav={nav} value={value} onPick={pick} collapsed={collapsed} workspace={workspace} workspaces={workspaces} user={user} />\n      </aside>\n\n      <Sheet open={drawer} onOpenChange={setDrawer}>\n        <SheetContent side=\"start\" className=\"w-72 p-0\" closeLabel=\"Close navigation\">\n          <SheetTitle className=\"sr-only\">Navigation</SheetTitle>\n          <SheetDescription className=\"sr-only\">Pages in {workspace}</SheetDescription>\n          <SidebarBody nav={nav} value={value} onPick={pick} collapsed={false} workspace={workspace} workspaces={workspaces} user={user} />\n        </SheetContent>\n      </Sheet>\n\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <header className=\"flex h-14 shrink-0 items-center gap-2 border-b px-3 sm:px-4\">\n          <Button variant=\"ghost\" size=\"icon\" className=\"md:hidden\" aria-label=\"Open navigation\" onClick={() => setDrawer(true)}><PanelLeft  className=\"rtl:-scale-x-100\"/></Button>\n          <Button variant=\"ghost\" size=\"icon\" className=\"hidden md:inline-flex\" aria-label={collapsed ? \"Expand sidebar\" : \"Collapse sidebar\"} aria-pressed={collapsed} onClick={() => setCollapsed((c) => !c)}><PanelLeft  className=\"rtl:-scale-x-100\"/></Button>\n          <h2 className=\"truncate text-sm font-medium\"><span className=\"text-muted-foreground hidden sm:inline\">{workspace} / </span>{page.label}</h2>\n          <div className=\"ms-auto flex items-center gap-1.5\">\n            <button\n              type=\"button\"\n              onClick={() => setPalette(true)}\n              className=\"text-muted-foreground hover:bg-accent focus-visible:ring-ring/50 hidden h-9 w-56 items-center gap-2 rounded-lg border px-3 text-sm outline-none transition-colors focus-visible:ring-[3px] sm:flex\"\n            >\n              <Search className=\"size-4\" aria-hidden=\"true\" />\n              <span className=\"flex-1 text-start\">Search</span>\n              <Kbd>⌘K</Kbd>\n            </button>\n            <Button variant=\"ghost\" size=\"icon\" className=\"sm:hidden\" aria-label=\"Search\" onClick={() => setPalette(true)}><Search /></Button>\n            <Button variant=\"ghost\" size=\"icon\" aria-label=\"Notifications\"><Bell /></Button>\n          </div>\n        </header>\n        <main id={mainId} tabIndex={0} aria-label={page.label} className=\"focus-visible:ring-ring/50 min-h-0 flex-1 overflow-auto p-4 outline-none focus-visible:ring-[3px] focus-visible:ring-inset sm:p-6\">\n          {children ? children(page) : <SamplePage page={page} />}\n        </main>\n      </div>\n\n      <CommandDialog open={palette} onOpenChange={setPalette} title=\"Command menu\">\n        <CommandInput placeholder=\"Jump to a page…\" />\n        <CommandList>\n          <CommandEmpty>No matching pages.</CommandEmpty>\n          <CommandGroup heading=\"Go to\">\n            {all.map((item) => (\n              <CommandItem key={item.id} value={item.label} onSelect={() => pick(item.id)}>\n                <span aria-hidden=\"true\">{item.icon}</span>\n                {item.label}\n                {item.id === value && <CommandShortcut>Current</CommandShortcut>}\n              </CommandItem>\n            ))}\n          </CommandGroup>\n        </CommandList>\n      </CommandDialog>\n    </div>\n  )\n}\n\nexport { AppShell1, type AppShell1Props, type AppShell1Group, type AppShell1Item }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/app-shell-1/app-shell-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "app shell",
      "sidebar",
      "dashboard layout",
      "navigation",
      "command menu",
      "workspace"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "The frame around an app. Pass nav=[{ label?, items: [{ id, label, icon?, badge? }] }], workspace, user and a children render function (page) => node; value / onValueChange control the open page.",
      "whenToUse": [
        "Dashboards, admin tools and any multi-page product",
        "A realistic frame for demos and screenshots"
      ],
      "whenNotToUse": [
        "Marketing sites (use header-2)",
        "Single-screen tools"
      ],
      "composesWith": [
        "dashboard-1",
        "dashboard-2",
        "settings-1",
        "billing-1",
        "mail-1",
        "kanban-1"
      ],
      "a11y": [
        {
          "keys": "Ctrl/⌘ + K",
          "action": "Opens the command menu to jump to any page"
        },
        {
          "keys": "Ctrl/⌘ + B",
          "action": "Collapses or expands the sidebar"
        },
        {
          "keys": "Tab",
          "action": "A skip link jumps past the sidebar; the open page is marked aria-current=page"
        }
      ],
      "customization": [
        "nav groups, workspace, workspaces, user",
        "children: (page) => node",
        "defaultCollapsed, height ('100svh' for a full-screen app)"
      ]
    },
    "examples": [
      "app-shell-1-demo",
      "app-shell-1-collapsed"
    ],
    "url": "https://ui.ballmac.com/components/app-shell-1"
  },
  "categories": [
    "blocks",
    "app-shell"
  ],
  "type": "registry:block"
}