{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"app-shell","type":"registry:ui","title":"App Shell","description":"A dashboard's frame in one tag: the sidebar, a bar that sticks, the page header and the page; on a phone the sidebar opens from a menu button.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/button.json","https://dashral.com/r/page-header.json","https://dashral.com/r/sheet.json","https://dashral.com/r/sidebar.json"],"files":[{"path":"registry/dashral/app-shell.tsx","type":"registry:file","target":"components/dashral/app-shell.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { MenuIcon } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { Button } from \"./button\"\nimport { PageHeader, type PageHeaderProps } from \"./page-header\"\nimport { Sheet } from \"./sheet\"\nimport { Sidebar, type SidebarProps } from \"./sidebar\"\n\ntype AppShellProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** The side navigation, as Sidebar's props: items, header (your logo), footer (the user), linkAs and the rest. */\n  sidebar: Omit<SidebarProps, \"collapsed\" | \"defaultCollapsed\" | \"onCollapsedChange\">\n  /** The bar along the top, beside the menu button on a phone: a search, notifications, the user. It sticks as the page scrolls. */\n  header?: React.ReactNode\n  /** The page's header under the bar, as PageHeader's props: title, description, breadcrumb, actions, tabs. */\n  page?: PageHeaderProps\n  /** The page itself. */\n  children?: React.ReactNode\n  /** The sidebar narrowed to its icons. Controlled; or set defaultCollapsed and let its toggle change it. */\n  collapsed?: boolean\n  defaultCollapsed?: boolean\n  onCollapsedChange?: (collapsed: boolean) => void\n  /**\n   * Inside a box of your own instead of the whole window: it fills the box's height and the page scrolls within it, the\n   * bar sticking at its top. For a preview, a demo or a panel; an app leaves it off and scrolls with the window.\n   */\n  contained?: boolean\n  classNames?: { sidebar?: string; header?: string; main?: string; page?: string }\n}\n\n/**\n * A dashboard's frame, in one tag: `<AppShell sidebar={{ items }} header={<Search />} page={{ title: \"Orders\" }}>…</AppShell>`.\n * The sidebar on the left (it narrows to its icons with its toggle), a bar along the top that sticks as the page\n * scrolls, the page's header and then the page. On a phone the sidebar moves into a sheet opened from the bar's menu\n * button, which closes again when a link in it is followed.\n */\nfunction AppShell({\n  sidebar,\n  header,\n  page,\n  children,\n  collapsed: collapsedProp,\n  defaultCollapsed = false,\n  onCollapsedChange,\n  contained = false,\n  className,\n  classNames,\n  ...props\n}: AppShellProps) {\n  const { t } = useLocale()\n  const [own, setOwn] = React.useState(defaultCollapsed)\n  const collapsed = collapsedProp ?? own\n  const [menuOpen, setMenuOpen] = React.useState(false)\n\n  function setCollapsed(next: boolean) {\n    if (collapsedProp === undefined) setOwn(next)\n    onCollapsedChange?.(next)\n  }\n\n  return (\n    <div data-slot=\"app-shell\" className={cn(\"flex w-full bg-background\", contained ? \"h-full min-h-0 overflow-hidden\" : \"min-h-svh\", className)} {...props}>\n      {/* The sidebar from lg up, as tall as the window while the page scrolls past it; in a box, stretched to its height\n          (a flex row stretches it: a percentage height wouldn't resolve against a height the box only gets from flex). */}\n      <div className={cn(\"hidden shrink-0 lg:flex\", !contained && \"sticky top-0 h-svh\")}>\n        <Sidebar {...sidebar} collapsed={collapsed} onCollapsedChange={setCollapsed} className={cn(\"h-full\", sidebar.className, classNames?.sidebar)} />\n      </div>\n      <div className={cn(\"flex min-w-0 flex-1 flex-col\", contained && \"overflow-y-auto\")}>\n        <header\n          data-slot=\"app-shell-header\"\n          className={cn(\n            \"sticky top-0 z-30 flex h-14 shrink-0 items-center gap-2 border-b border-divider bg-background/85 px-4 backdrop-blur-md md:px-6\",\n            classNames?.header\n          )}\n        >\n          <Sheet\n            open={menuOpen}\n            onOpenChange={setMenuOpen}\n            side=\"left\"\n            padding=\"none\"\n            title={sidebar[\"aria-label\"] ?? t(\"navigation\")}\n            classNames={{ title: \"sr-only\" }}\n            trigger={\n              <Button variant=\"ghost\" size=\"icon-sm\" aria-label={t(\"openNavigation\")} className=\"-ms-1.5 lg:hidden\">\n                <MenuIcon />\n              </Button>\n            }\n          >\n            {/* Following a link closes the sheet, so the page it opens is in view. */}\n            <div\n              className=\"h-full\"\n              onClickCapture={(event) => {\n                if ((event.target as Element).closest(\"a[href]\")) setMenuOpen(false)\n              }}\n            >\n              <Sidebar {...sidebar} collapsible={false} className={cn(\"h-full w-full border-e-0\", sidebar.className)} />\n            </div>\n          </Sheet>\n          <div className=\"flex min-w-0 flex-1 items-center gap-2\">{header}</div>\n        </header>\n        <main data-slot=\"app-shell-main\" className={cn(\"w-full flex-1 space-y-6 p-4 md:p-6\", classNames?.main)}>\n          {page && <PageHeader {...page} className={cn(page.className, classNames?.page)} />}\n          {children}\n        </main>\n      </div>\n    </div>\n  )\n}\n\nexport { AppShell, type AppShellProps }\n"}]}