{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-shell",
  "title": "App Shell",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "The frame of an application page: skip link, sticky header, sidebar that becomes a sheet on small screens, main area, optional aside and footer.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/sheet.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/app-shell.tsx",
      "content": "// Ballmac UI: App Shell. https://ui.ballmac.com/components/app-shell\n\"use client\";\n\nimport * as React from \"react\";\nimport { PanelLeft } from \"lucide-react\";\nimport { Sheet, SheetContent, SheetDescription, SheetTitle } from \"@/components/ballmac/sheet\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype AppShellContextValue = { sidebarOpen: boolean; setSidebarOpen: (open: boolean) => void };\nconst AppShellContext = React.createContext<AppShellContextValue | null>(null);\n\nfunction useAppShell() {\n  const context = React.useContext(AppShellContext);\n  if (!context) throw new Error(\"App shell parts must be used inside <AppShell>\");\n  return context;\n}\n\ntype AppShellProps = React.ComponentProps<\"div\"> & {\n  /** Height of the header, used to offset sticky side panels. Any CSS length. */\n  headerHeight?: string;\n  /** Width of the sidebar column from the `lg` breakpoint up. */\n  sidebarWidth?: string;\n  /** Width of the aside column from the `xl` breakpoint up. */\n  asideWidth?: string;\n};\n\n/**\n * The frame of an application page: skip link, sticky header, sidebar, main content, optional aside and footer.\n * The sidebar becomes a sheet below `lg`. Panels stick below the header and scroll on their own.\n */\nfunction AppShell({\n  headerHeight = \"3.5rem\",\n  sidebarWidth = \"15rem\",\n  asideWidth = \"18rem\",\n  className,\n  style,\n  children,\n  ...props\n}: AppShellProps) {\n  const [sidebarOpen, setSidebarOpen] = React.useState(false);\n  const value = React.useMemo(() => ({ sidebarOpen, setSidebarOpen }), [sidebarOpen]);\n  return (\n    <AppShellContext.Provider value={value}>\n      <div\n        data-slot=\"app-shell\"\n        style={\n          {\n            \"--app-header-h\": headerHeight,\n            \"--app-sidebar-w\": sidebarWidth,\n            \"--app-aside-w\": asideWidth,\n            ...style,\n          } as React.CSSProperties\n        }\n        className={cn(\n          \"grid min-h-svh w-full grid-cols-1 grid-rows-[auto_1fr_auto] bg-background text-foreground\",\n          \"lg:grid-cols-[var(--app-sidebar-w)_minmax(0,1fr)]\",\n          \"has-[[data-slot=app-shell-aside]]:xl:grid-cols-[var(--app-sidebar-w)_minmax(0,1fr)_var(--app-aside-w)]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </AppShellContext.Provider>\n  );\n}\n\ntype AppShellSkipLinkProps = React.ComponentProps<\"a\">;\n/** Becomes visible on keyboard focus and jumps to the main content. */\nfunction AppShellSkipLink({ className, children = \"Skip to content\", href = \"#app-main\", ...props }: AppShellSkipLinkProps) {\n  return (\n    <a\n      data-slot=\"app-shell-skip-link\"\n      href={href}\n      className={cn(\n        \"sr-only z-50 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground focus:not-sr-only focus:fixed focus:top-2 focus:start-2 focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </a>\n  );\n}\n\ntype AppShellHeaderProps = React.ComponentProps<\"header\">;\nfunction AppShellHeader({ className, ...props }: AppShellHeaderProps) {\n  return (\n    <header\n      data-slot=\"app-shell-header\"\n      className={cn(\n        \"sticky top-0 z-30 flex h-(--app-header-h) items-center gap-3 border-b bg-background/85 px-4 backdrop-blur-md lg:col-span-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype AppShellSidebarTriggerProps = React.ComponentProps<\"button\">;\n/** Opens the sidebar sheet below `lg`. Hidden on wider screens. */\nfunction AppShellSidebarTrigger({ className, onClick, ...props }: AppShellSidebarTriggerProps) {\n  const msg = useMessages()\n  const { sidebarOpen, setSidebarOpen } = useAppShell();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"app-shell-sidebar-trigger\"\n      aria-label={msg(\"app-shell.openNavigation\", \"Open navigation\")}\n      aria-expanded={sidebarOpen}\n      onClick={(event) => {\n        onClick?.(event);\n        setSidebarOpen(true);\n      }}\n      className={cn(\n        \"-ms-1 inline-flex size-9 items-center justify-center rounded-md outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 lg:hidden\",\n        className,\n      )}\n      {...props}\n    >\n      <PanelLeft aria-hidden=\"true\" className=\"size-5 rtl:-scale-x-100\" />\n    </button>\n  );\n}\n\ntype AppShellSidebarProps = React.ComponentProps<\"aside\"> & {\n  /** Accessible name of the landmark and the mobile sheet. */\n  label?: string;\n};\nfunction AppShellSidebar({ label, className, children, ...props }: AppShellSidebarProps) {\n  const msg = useMessages()\n  label ??= msg(\"app-shell.label\", \"Sidebar\")\n  const { sidebarOpen, setSidebarOpen } = useAppShell();\n  return (\n    <>\n      <aside\n        data-slot=\"app-shell-sidebar\"\n        aria-label={label}\n        className={cn(\n          \"sticky top-(--app-header-h) hidden h-[calc(100svh-var(--app-header-h))] flex-col overflow-y-auto border-e bg-card/50 lg:flex\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </aside>\n      <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n        <SheetContent side=\"start\" showCloseButton={false} className=\"w-72 gap-0 p-0 sm:w-72 lg:hidden\">\n          <SheetTitle className=\"sr-only\">{label}</SheetTitle>\n          <SheetDescription className=\"sr-only\">{msg(\"app-shell.navigation\", \"Navigation\")}</SheetDescription>\n          <div className=\"flex h-full flex-col overflow-y-auto\" onClick={(e) => (e.target as HTMLElement).closest(\"a\") && setSidebarOpen(false)}>\n            {children}\n          </div>\n        </SheetContent>\n      </Sheet>\n    </>\n  );\n}\n\ntype AppShellMainProps = React.ComponentProps<\"main\">;\nfunction AppShellMain({ className, ...props }: AppShellMainProps) {\n  return (\n    <main\n      id=\"app-main\"\n      tabIndex={-1}\n      data-slot=\"app-shell-main\"\n      className={cn(\"min-w-0 p-4 outline-none sm:p-6 lg:p-8\", className)}\n      {...props}\n    />\n  );\n}\n\ntype AppShellAsideProps = React.ComponentProps<\"aside\"> & { label?: string };\n/** A right column shown from `xl` up (details, activity, table of contents). */\nfunction AppShellAside({ label, className, ...props }: AppShellAsideProps) {\n  const msg = useMessages()\n  label ??= msg(\"app-shell.label2\", \"Details\")\n  return (\n    <aside\n      data-slot=\"app-shell-aside\"\n      aria-label={label}\n      className={cn(\n        \"sticky top-(--app-header-h) hidden h-[calc(100svh-var(--app-header-h))] overflow-y-auto border-s p-5 xl:block\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\ntype AppShellFooterProps = React.ComponentProps<\"footer\">;\nfunction AppShellFooter({ className, ...props }: AppShellFooterProps) {\n  return (\n    <footer\n      data-slot=\"app-shell-footer\"\n      className={cn(\"border-t px-4 py-3 text-xs text-muted-foreground sm:px-6 lg:col-span-full\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  AppShell,\n  AppShellSkipLink,\n  AppShellHeader,\n  AppShellSidebarTrigger,\n  AppShellSidebar,\n  AppShellMain,\n  AppShellAside,\n  AppShellFooter,\n  useAppShell,\n  type AppShellProps,\n  type AppShellSkipLinkProps,\n  type AppShellHeaderProps,\n  type AppShellSidebarTriggerProps,\n  type AppShellSidebarProps,\n  type AppShellMainProps,\n  type AppShellAsideProps,\n  type AppShellFooterProps,\n};\n",
      "type": "registry:ui",
      "target": "@components/ballmac/app-shell.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "layout",
      "dashboard",
      "sidebar",
      "landmarks"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "AppShell > AppShellSkipLink, AppShellHeader (with AppShellSidebarTrigger), AppShellSidebar, AppShellMain, AppShellAside, AppShellFooter. Sizes are CSS variables set by props.",
      "whenToUse": [
        "Admin and dashboard pages",
        "Any page with a sidebar, a main column and optional details"
      ],
      "whenNotToUse": [
        "A collapsible icon-rail sidebar; use sidebar",
        "Marketing pages; use navbar"
      ],
      "composesWith": [
        "sidebar",
        "navbar",
        "sheet"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "The first stop is the skip link; it jumps to the main content"
        },
        {
          "keys": "Screen readers",
          "action": "Header, sidebar, main, aside and footer are landmarks"
        },
        {
          "keys": "Escape",
          "action": "Closes the mobile sidebar"
        }
      ],
      "customization": [
        "headerHeight, sidebarWidth, asideWidth",
        "sticky panels scroll on their own",
        "hide the aside by leaving it out"
      ]
    },
    "examples": [
      "app-shell-demo",
      "app-shell-states"
    ],
    "url": "https://ui.ballmac.com/components/app-shell"
  },
  "categories": [
    "layout"
  ],
  "type": "registry:ui"
}