{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "dashboard-shell",
  "type": "registry:block",
  "title": "Dashboard shell",
  "description": "A responsive application shell with persistent navigation, a mobile sheet, top bar, and page header.",
  "dependencies": [
    "@tabler/icons-react"
  ],
  "registryDependencies": [
    "https://abackui.com/r/utils.json",
    "https://abackui.com/r/avatar.json",
    "https://abackui.com/r/button.json",
    "https://abackui.com/r/dropdown-menu.json",
    "https://abackui.com/r/sheet.json"
  ],
  "categories": [
    "Application"
  ],
  "docs": "https://abackui.com/docs/blocks/dashboard-shell",
  "meta": {
    "status": "beta"
  },
  "files": [
    {
      "path": "components/blocks/dashboard-shell.tsx",
      "content": "\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport {\n  IconChevronDown,\n  IconLogout,\n  IconMenu,\n  IconSearch,\n  IconSettings,\n} from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Sheet,\n  SheetContent,\n  SheetDescription,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n} from \"@/components/ui/sheet\";\n\nexport interface DashboardNavItem {\n  title: string;\n  href: string;\n  icon?: ReactNode;\n  /** A short count or state, such as an unread total. */\n  badge?: string;\n}\n\nexport interface DashboardUser {\n  name: string;\n  email: string;\n  initials?: string;\n}\n\nexport interface DashboardShellProps {\n  children: ReactNode;\n  nav: DashboardNavItem[];\n  /** The `href` of the current section. Matched exactly. */\n  activeHref?: string;\n  title: string;\n  description?: string;\n  actions?: ReactNode;\n  appName?: string;\n  user?: DashboardUser;\n  /** A real destination for account settings. The menu item is omitted when absent. */\n  accountHref?: string;\n  onSignOut?: () => void;\n  /** Renders a search trigger in the top bar when provided. */\n  onSearch?: () => void;\n}\n\nfunction initialsFor(user: DashboardUser) {\n  if (user.initials) return user.initials;\n  return user.name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((part) => part[0]?.toUpperCase() ?? \"\")\n    .join(\"\");\n}\n\nfunction NavLinks({\n  nav,\n  activeHref,\n  onNavigate,\n}: {\n  nav: DashboardNavItem[];\n  activeHref?: string;\n  onNavigate?: () => void;\n}) {\n  return (\n    <ul className=\"flex flex-col gap-0.5\">\n      {nav.map((item) => {\n        const active = item.href === activeHref;\n        return (\n          <li key={item.href}>\n            <a\n              href={item.href}\n              aria-current={active ? \"page\" : undefined}\n              onClick={onNavigate}\n              className={\n                active\n                  ? \"flex items-center gap-2.5 border-l-2 border-primary-500 bg-primary-50 px-3 py-2 text-sm font-medium text-primary-700 transition-colors dark:bg-primary-950/40 dark:text-primary-300\"\n                  : \"flex items-center gap-2.5 border-l-2 border-transparent px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground\"\n              }\n            >\n              {item.icon ? (\n                <span aria-hidden=\"true\" className=\"shrink-0 [&_svg]:size-4\">\n                  {item.icon}\n                </span>\n              ) : null}\n              <span className=\"min-w-0 flex-1 truncate\">{item.title}</span>\n              {item.badge ? (\n                <span className=\"shrink-0 bg-muted px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-muted-foreground\">\n                  {item.badge}\n                </span>\n              ) : null}\n            </a>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n\nfunction UserMenu({\n  user,\n  accountHref,\n  onSignOut,\n}: {\n  user: DashboardUser;\n  accountHref?: string;\n  onSignOut?: () => void;\n}) {\n  const identity = (\n    <>\n      <Avatar size=\"sm\">\n        <AvatarFallback aria-hidden=\"true\">{initialsFor(user)}</AvatarFallback>\n      </Avatar>\n      <span className=\"min-w-0 flex-1 text-left\">\n        <span className=\"block truncate text-sm font-medium text-foreground\">{user.name}</span>\n        <span className=\"block truncate text-xs text-muted-foreground\">{user.email}</span>\n      </span>\n    </>\n  );\n\n  if (!accountHref && !onSignOut) {\n    return <div className=\"flex items-center gap-2.5\">{identity}</div>;\n  }\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <button\n          type=\"button\"\n          className=\"flex w-full items-center gap-2.5 p-1 text-left transition-colors hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card focus-visible:outline-none\"\n          aria-label={`Account menu for ${user.name}`}\n        >\n          {identity}\n          <IconChevronDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        </button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"w-56\">\n        <DropdownMenuLabel>\n          <span className=\"block truncate text-sm font-medium\">{user.name}</span>\n          <span className=\"block truncate text-xs font-normal text-muted-foreground\">\n            {user.email}\n          </span>\n        </DropdownMenuLabel>\n        <DropdownMenuSeparator />\n        {accountHref ? (\n          <DropdownMenuItem asChild>\n            <a href={accountHref}>\n              <IconSettings aria-hidden=\"true\" />\n              Account settings\n            </a>\n          </DropdownMenuItem>\n        ) : null}\n        {onSignOut ? (\n          <DropdownMenuItem variant=\"destructive\" onSelect={onSignOut}>\n            <IconLogout aria-hidden=\"true\" />\n            Sign out\n          </DropdownMenuItem>\n        ) : null}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\n/**\n * A responsive application shell: a persistent sidebar from `md` up, a sheet\n * below it, a top bar, and the page header. It owns no routing, data, or auth.\n * Pass real `href`s, your user, and the page content; `onSearch` and\n * `onSignOut` are optional, so the shell never renders a control without an\n * action behind it.\n *\n * The content region is a div, not a landmark. Wrap the shell in your app's\n * `<main>`, or replace the content container with `<main>` when the shell owns\n * the whole page.\n */\nexport function DashboardShellBlock({\n  children,\n  nav,\n  activeHref,\n  title,\n  description,\n  actions,\n  appName = \"Aback\",\n  user,\n  accountHref,\n  onSignOut,\n  onSearch,\n}: DashboardShellProps) {\n  const [mobileOpen, setMobileOpen] = useState(false);\n  const brandMark = (\n    <span\n      aria-hidden=\"true\"\n      className=\"flex size-6 shrink-0 items-center justify-center bg-primary text-[11px] font-bold text-primary-foreground\"\n    >\n      {appName.slice(0, 1).toUpperCase()}\n    </span>\n  );\n\n  return (\n    <div className=\"flex min-h-[640px] w-full border border-border bg-background text-foreground\">\n      <aside className=\"hidden w-56 shrink-0 flex-col border-r border-border bg-card md:flex\">\n        <div className=\"flex h-14 items-center gap-2 border-b border-border px-4\">\n          {brandMark}\n          <span className=\"truncate text-sm font-semibold\">{appName}</span>\n        </div>\n\n        <nav aria-label=\"Primary\" className=\"flex-1 overflow-y-auto p-2\">\n          <NavLinks nav={nav} activeHref={activeHref} />\n        </nav>\n\n        {user ? (\n          <div className=\"border-t border-border p-2.5\">\n            <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n          </div>\n        ) : null}\n      </aside>\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 border-border px-3 sm:px-4\">\n          <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>\n            <SheetTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon-sm\" className=\"md:hidden\" aria-label=\"Open navigation\">\n                <IconMenu aria-hidden=\"true\" />\n              </Button>\n            </SheetTrigger>\n            <SheetContent side=\"left\" className=\"w-72 p-0\">\n              <SheetHeader className=\"border-b border-border p-4\">\n                <SheetTitle className=\"flex items-center gap-2 text-sm font-semibold\">\n                  {brandMark}\n                  {appName}\n                </SheetTitle>\n                <SheetDescription className=\"sr-only\">\n                  Sections available in {appName}.\n                </SheetDescription>\n              </SheetHeader>\n              <nav aria-label=\"Primary\" className=\"p-2\">\n                <NavLinks\n                  nav={nav}\n                  activeHref={activeHref}\n                  onNavigate={() => setMobileOpen(false)}\n                />\n              </nav>\n              {user ? (\n                <div className=\"mt-auto border-t border-border p-3\">\n                  <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n                </div>\n              ) : null}\n            </SheetContent>\n          </Sheet>\n\n          <span className=\"truncate text-sm font-semibold md:hidden\">{appName}</span>\n\n          {onSearch ? (\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={onSearch}\n              className=\"ml-auto gap-2 text-muted-foreground\"\n            >\n              <IconSearch aria-hidden=\"true\" />\n              <span className=\"hidden sm:inline\">Search</span>\n              <span className=\"sr-only sm:hidden\">Search</span>\n            </Button>\n          ) : null}\n\n          {user ? (\n            <div className=\"ml-auto md:hidden\">\n              <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n            </div>\n          ) : null}\n        </header>\n\n        <div className=\"flex flex-1 flex-col\">\n          <div className=\"flex flex-wrap items-start justify-between gap-3 border-b border-border px-4 py-5 sm:px-6\">\n            <div className=\"min-w-0\">\n              <h1 className=\"truncate text-lg font-semibold tracking-tight\">{title}</h1>\n              {description ? (\n                <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p>\n              ) : null}\n            </div>\n            {actions ? <div className=\"flex shrink-0 items-center gap-2\">{actions}</div> : null}\n          </div>\n\n          <div className=\"flex-1 p-4 sm:p-6\">{children}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:block",
      "target": "components/blocks/dashboard-shell.tsx"
    }
  ]
}
