{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-sidebar",
  "title": "App Sidebar",
  "description": "Authenticated app-shell navigation: brand block, labelled nav groups with active state from usePathname, user menu with links and sign-out callback; icon rail on desktop, drawer on mobile. Brand, groups, user and callbacks are props.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@sav/dropdown-menu",
    "@sav/sidebar",
    "@sav/user-avatar"
  ],
  "files": [
    {
      "path": "registry/sav/components/app-sidebar.tsx",
      "content": "// @sav-registry: app-sidebar\n\"use client\"\n\n// Authenticated app shell navigation: a collapsible left sidebar that collapses\n// to an icon rail on desktop and becomes an off-canvas drawer on mobile (both\n// handled by the Sidebar/SidebarProvider mechanism). Brand, navigation and the\n// user menu all arrive as plain-serializable props plus callbacks, so nothing\n// here belongs to one app. Active state is derived from usePathname. Role\n// filtering is the consumer's job: pass only the groups the current user may\n// see.\n\nimport Link from \"next/link\"\nimport { usePathname } from \"next/navigation\"\nimport { ChevronsUpDownIcon, LogOutIcon } from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\n\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/sav/ui/dropdown-menu\"\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarRail,\n  useSidebar,\n} from \"@/registry/sav/ui/sidebar\"\nimport { UserAvatar } from \"@/registry/sav/components/user-avatar\"\n\nexport type SidebarUser = {\n  name: string\n  email: string\n  firstName: string\n  lastName: string\n  avatarUrl: string | null\n}\n\nexport type SidebarNavLink = {\n  href: string\n  label: string\n  icon: LucideIcon\n  /** Match the exact path only (default: the path and every sub-path). */\n  exact?: boolean\n}\n\nexport type SidebarNavGroup = {\n  /** Section label; omit for an unlabelled top-level group (e.g. Dashboard). */\n  label?: string\n  items: SidebarNavLink[]\n}\n\nexport type SidebarBrand = {\n  /** Product name, set in the heading font. */\n  name: string\n  /** Second line under the name (company, edition, environment). */\n  tagline?: string\n  /** Where the brand block links to. */\n  href?: string\n  /** Icon drawn in the brand square; ignored when `mark` is given. */\n  icon?: LucideIcon\n  /** Custom brand square content (e.g. an <img> of the app mark). */\n  mark?: React.ReactNode\n}\n\nexport type SidebarUserLink = {\n  href: string\n  label: string\n  icon?: LucideIcon\n}\n\nexport function AppSidebar({\n  brand,\n  groups,\n  user,\n  userLinks = [],\n  onSignOut,\n  signOutLabel = \"Sign out\",\n  collapsible = \"icon\",\n  isActive,\n  header,\n  children,\n}: {\n  brand: SidebarBrand\n  groups: SidebarNavGroup[]\n  /** Extra content under the brand block in the header (e.g. an entity switcher). */\n  header?: React.ReactNode\n  /** Omit to render no footer menu. */\n  user?: SidebarUser\n  /** Links in the user menu (account, settings, …). */\n  userLinks?: SidebarUserLink[]\n  /** Called from the user menu; the consumer owns the sign-out mechanics. */\n  onSignOut?: () => void | Promise<void>\n  signOutLabel?: string\n  collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n  /** Override the default `pathname === href || pathname.startsWith(href + \"/\")`. */\n  isActive?: (link: SidebarNavLink, pathname: string) => boolean\n  /** Extra content rendered after the navigation groups. */\n  children?: React.ReactNode\n}) {\n  const pathname = usePathname()\n  const { setOpenMobile } = useSidebar()\n  const active = (link: SidebarNavLink) =>\n    isActive\n      ? isActive(link, pathname)\n      : pathname === link.href ||\n        (!link.exact && pathname.startsWith(`${link.href}/`))\n  // Close the mobile drawer after a nav tap; no-op on desktop.\n  const closeMobile = () => setOpenMobile(false)\n  const BrandIcon = brand.icon\n\n  return (\n    <Sidebar collapsible={collapsible}>\n      <SidebarHeader>\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <SidebarMenuButton\n              size=\"lg\"\n              tooltip={brand.name}\n              className=\"data-active:bg-transparent data-active:font-normal\"\n              render={\n                <Link href={brand.href ?? \"/\"} onClick={closeMobile}>\n                  <div className=\"flex aspect-square size-8 items-center justify-center bg-sidebar-primary text-sidebar-primary-foreground\">\n                    {brand.mark ? (\n                      brand.mark\n                    ) : BrandIcon ? (\n                      <BrandIcon className=\"size-4\" />\n                    ) : (\n                      <span className=\"font-heading text-sm font-semibold\">\n                        {brand.name.charAt(0).toUpperCase()}\n                      </span>\n                    )}\n                  </div>\n                  <div className=\"grid flex-1 text-left leading-tight\">\n                    <span className=\"truncate font-heading text-sm font-semibold\">\n                      {brand.name}\n                    </span>\n                    {brand.tagline ? (\n                      <span className=\"truncate text-xs text-sidebar-foreground/70\">\n                        {brand.tagline}\n                      </span>\n                    ) : null}\n                  </div>\n                </Link>\n              }\n            />\n          </SidebarMenuItem>\n        </SidebarMenu>\n        {header}\n      </SidebarHeader>\n\n      <SidebarContent>\n        {groups.map((group, index) => {\n          if (group.items.length === 0) return null\n          return (\n            <SidebarGroup key={group.label ?? `group-${index}`}>\n              {group.label ? (\n                <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n              ) : null}\n              <SidebarGroupContent>\n                <SidebarMenu>\n                  {group.items.map((item) => (\n                    <SidebarMenuItem key={item.href}>\n                      <SidebarMenuButton\n                        isActive={active(item)}\n                        tooltip={item.label}\n                        render={\n                          <Link href={item.href} onClick={closeMobile}>\n                            <item.icon />\n                            <span>{item.label}</span>\n                          </Link>\n                        }\n                      />\n                    </SidebarMenuItem>\n                  ))}\n                </SidebarMenu>\n              </SidebarGroupContent>\n            </SidebarGroup>\n          )\n        })}\n        {children}\n      </SidebarContent>\n\n      {user ? (\n        <SidebarFooter>\n          <SidebarMenu>\n            <SidebarMenuItem>\n              <NavUser\n                user={user}\n                links={userLinks}\n                onSignOut={onSignOut}\n                signOutLabel={signOutLabel}\n              />\n            </SidebarMenuItem>\n          </SidebarMenu>\n        </SidebarFooter>\n      ) : null}\n\n      <SidebarRail />\n    </Sidebar>\n  )\n}\n\nfunction NavUser({\n  user,\n  links,\n  onSignOut,\n  signOutLabel,\n}: {\n  user: SidebarUser\n  links: SidebarUserLink[]\n  onSignOut?: () => void | Promise<void>\n  signOutLabel: string\n}) {\n  const { isMobile, setOpenMobile } = useSidebar()\n  const closeMobile = () => setOpenMobile(false)\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        render={\n          <SidebarMenuButton\n            size=\"lg\"\n            className=\"data-[popup-open]:bg-sidebar-accent data-[popup-open]:text-sidebar-accent-foreground\"\n          >\n            <UserAvatar\n              src={user.avatarUrl}\n              firstName={user.firstName}\n              lastName={user.lastName}\n              email={user.email}\n              size=\"sm\"\n            />\n            <div className=\"grid flex-1 text-left leading-tight\">\n              <span className=\"truncate text-sm font-medium\">{user.name}</span>\n              <span className=\"truncate text-xs text-sidebar-foreground/70\">\n                {user.email}\n              </span>\n            </div>\n            <ChevronsUpDownIcon className=\"ml-auto size-4\" />\n          </SidebarMenuButton>\n        }\n      />\n      <DropdownMenuContent\n        side={isMobile ? \"bottom\" : \"right\"}\n        align=\"end\"\n        sideOffset={4}\n        className=\"min-w-56\"\n      >\n        <div className=\"flex items-center gap-2 px-2 py-2 text-left\">\n          <UserAvatar\n            src={user.avatarUrl}\n            firstName={user.firstName}\n            lastName={user.lastName}\n            email={user.email}\n            size=\"sm\"\n          />\n          <div className=\"grid flex-1 leading-tight\">\n            <span className=\"truncate text-sm font-medium normal-case\">\n              {user.name}\n            </span>\n            <span className=\"truncate text-xs font-normal tracking-normal text-muted-foreground normal-case\">\n              {user.email}\n            </span>\n          </div>\n        </div>\n        {links.length > 0 ? (\n          <>\n            <DropdownMenuSeparator />\n            {links.map((link) => {\n              const Icon = link.icon\n              return (\n                <DropdownMenuItem\n                  key={link.href}\n                  render={\n                    <Link href={link.href} onClick={closeMobile}>\n                      {Icon ? <Icon /> : null}\n                      {link.label}\n                    </Link>\n                  }\n                />\n              )\n            })}\n          </>\n        ) : null}\n        {onSignOut ? (\n          <>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              variant=\"destructive\"\n              onClick={() => void onSignOut()}\n            >\n              <LogOutIcon />\n              {signOutLabel}\n            </DropdownMenuItem>\n          </>\n        ) : null}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/app-sidebar.tsx"
    }
  ],
  "categories": [
    "patterns"
  ],
  "type": "registry:component"
}