{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "app-sidebar",
  "title": "App Sidebar",
  "author": "@fma-ui/ui",
  "description": "An app-shell sidebar block, collapsible to icons: a team switcher (or a static logo), one navigation section per `data.nav` entry and a user menu. Driven by an `AppSidebarData` object, so teams, sections and items are data. Items with sub-items expand inline (`variant: \"collapse\"`) or open a menu (`\"dropdown\"`, and always while collapsed to icons). Links are react-aria `Link`s: wrap the app in `RouterProvider` for client-side routing. `onTeamChange`, `onAddTeam` and `onUserAction` handle the menus. Render inside a `SidebarProvider`.",
  "dependencies": [
    "lucide-react@^1.52.0"
  ],
  "registryDependencies": [
    "@fma-ui/sidebar",
    "@fma-ui/collapsible",
    "@fma-ui/dropdown-menu",
    "@fma-ui/avatar"
  ],
  "files": [
    {
      "path": "src/blocks/app-sidebar/app-sidebar.tsx",
      "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarHeader,\n  SidebarRail,\n} from \"@/core/sidebar/sidebar\";\nimport { NavSection } from \"./nav-section\";\nimport { NavUser } from \"./nav-user\";\nimport { TeamSwitcher } from \"./team-switcher\";\nimport type { AppSidebarData, AppSidebarTeam, AppSidebarUserAction } from \"./types\";\n\ntype AppSidebarProps = React.ComponentProps<typeof Sidebar> & {\n  data: AppSidebarData;\n  onTeamChange?: (team: AppSidebarTeam) => void;\n  /** Shows an \"Add team\" entry in the team switcher. */\n  onAddTeam?: () => void;\n  onUserAction?: (action: AppSidebarUserAction) => void;\n};\n\n/**\n * App-shell sidebar (shadcn \"sidebar-07\"): collapsible to icons, with a team\n * switcher or logo, one section per `data.nav` entry and a user menu.\n * Render it inside a `SidebarProvider`.\n */\nexport function AppSidebar({\n  data,\n  onTeamChange,\n  onAddTeam,\n  onUserAction,\n  ...props\n}: AppSidebarProps) {\n  return (\n    <Sidebar collapsible=\"icon\" {...props}>\n      <SidebarHeader>\n        <TeamSwitcher\n          teams={data.teams}\n          logo={data.logo}\n          onTeamChange={onTeamChange}\n          onAddTeam={onAddTeam}\n        />\n      </SidebarHeader>\n      <SidebarContent>\n        {data.nav.map((section) => (\n          <NavSection key={section.title} section={section} />\n        ))}\n      </SidebarContent>\n      <SidebarFooter>\n        <NavUser user={data.user} onAction={onUserAction} />\n      </SidebarFooter>\n      <SidebarRail />\n    </Sidebar>\n  );\n}\n\nexport type * from \"./types\";\nexport type { AppSidebarProps };\n",
      "type": "registry:component",
      "target": "components/fma-ui/app-sidebar/app-sidebar.tsx"
    },
    {
      "path": "src/blocks/app-sidebar/nav-section.tsx",
      "content": "\"use client\";\n\nimport { ChevronRightIcon, MoreHorizontalIcon } from \"lucide-react\";\nimport { Collapsible, CollapsibleContent } from \"@/core/collapsible/collapsible\";\nimport {\n  DropdownMenu,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/core/dropdown-menu/dropdown-menu\";\nimport {\n  SidebarGroup,\n  SidebarGroupLabel,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarMenuSub,\n  SidebarMenuSubButton,\n  SidebarMenuSubItem,\n  useSidebar,\n} from \"@/core/sidebar/sidebar\";\nimport type { AppSidebarNavItem, AppSidebarNavSection } from \"./types\";\n\nexport function NavSection({ section }: { section: AppSidebarNavSection }) {\n  const { isMobile, state } = useSidebar();\n  const isIconCollapsed = state === \"collapsed\" && !isMobile;\n\n  return (\n    <SidebarGroup>\n      <SidebarGroupLabel>{section.title}</SidebarGroupLabel>\n      <SidebarMenu>\n        {section.items.map((item) => {\n          if (!item.items?.length) {\n            return (\n              <SidebarMenuItem key={item.title}>\n                <SidebarMenuButton\n                  href={item.url}\n                  isActive={item.isActive}\n                  aria-current={item.isActive ? \"page\" : undefined}\n                  tooltip={item.title}\n                >\n                  {item.icon}\n                  <span>{item.title}</span>\n                </SidebarMenuButton>\n              </SidebarMenuItem>\n            );\n          }\n          // Icon-only sidebars have no room for inline sub-items.\n          const variant = isIconCollapsed ? \"dropdown\" : (item.variant ?? \"collapse\");\n          return variant === \"dropdown\" ? (\n            <DropdownItem key={item.title} item={item} withLabel={isIconCollapsed} />\n          ) : (\n            <CollapseItem key={item.title} item={item} />\n          );\n        })}\n      </SidebarMenu>\n    </SidebarGroup>\n  );\n}\n\nfunction CollapseItem({ item }: { item: AppSidebarNavItem }) {\n  return (\n    <SidebarMenuItem>\n      <Collapsible defaultExpanded={item.isActive} className=\"group/collapsible\">\n        <SidebarMenuButton slot=\"trigger\" tooltip={item.title}>\n          {item.icon}\n          <span>{item.title}</span>\n          <ChevronRightIcon\n            aria-hidden=\"true\"\n            className=\"ml-auto transition-transform duration-200 group-data-expanded/collapsible:rotate-90\"\n          />\n        </SidebarMenuButton>\n        <CollapsibleContent>\n          <SidebarMenuSub>\n            {item.items?.map((subItem) => (\n              <SidebarMenuSubItem key={subItem.title}>\n                <SidebarMenuSubButton href={subItem.url} className=\"w-full\">\n                  {subItem.icon}\n                  <span>{subItem.title}</span>\n                </SidebarMenuSubButton>\n              </SidebarMenuSubItem>\n            ))}\n          </SidebarMenuSub>\n        </CollapsibleContent>\n      </Collapsible>\n    </SidebarMenuItem>\n  );\n}\n\nfunction DropdownItem({ item, withLabel }: { item: AppSidebarNavItem; withLabel: boolean }) {\n  const { isMobile } = useSidebar();\n  return (\n    <SidebarMenuItem>\n      <DropdownMenuTrigger>\n        <SidebarMenuButton\n          tooltip={item.title}\n          className=\"aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground\"\n        >\n          {item.icon}\n          <span>{item.title}</span>\n          <MoreHorizontalIcon aria-hidden=\"true\" className=\"ml-auto\" />\n        </SidebarMenuButton>\n        <DropdownMenu\n          aria-label={item.title}\n          placement={isMobile ? \"bottom start\" : \"end top\"}\n          className=\"w-fit min-w-40\"\n        >\n          {withLabel && (\n            <>\n              <DropdownMenuLabel>{item.title}</DropdownMenuLabel>\n              <DropdownMenuSeparator />\n            </>\n          )}\n          {item.items?.map((subItem) => (\n            <DropdownMenuItem key={subItem.title} id={subItem.title} href={subItem.url}>\n              {subItem.icon}\n              <span>{subItem.title}</span>\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenu>\n      </DropdownMenuTrigger>\n    </SidebarMenuItem>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/fma-ui/app-sidebar/nav-section.tsx"
    },
    {
      "path": "src/blocks/app-sidebar/nav-user.tsx",
      "content": "\"use client\";\n\nimport {\n  BadgeCheckIcon,\n  BellIcon,\n  ChevronsUpDownIcon,\n  CreditCardIcon,\n  LogOutIcon,\n  SparklesIcon,\n} from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/core/avatar/avatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/core/dropdown-menu/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@/core/sidebar/sidebar\";\nimport type { AppSidebarUser, AppSidebarUserAction } from \"./types\";\n\nfunction initials(name: string) {\n  return name\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((word) => word[0]?.toUpperCase() ?? \"\")\n    .join(\"\");\n}\n\nfunction UserSummary({ user }: { user: AppSidebarUser }) {\n  return (\n    <>\n      <Avatar>\n        {user.avatar && <AvatarImage src={user.avatar} alt=\"\" />}\n        <AvatarFallback>{initials(user.name)}</AvatarFallback>\n      </Avatar>\n      <div className=\"grid flex-1 text-left text-sm leading-tight\">\n        <span className=\"truncate font-medium\">{user.name}</span>\n        <span className=\"truncate text-xs\">{user.email}</span>\n      </div>\n    </>\n  );\n}\n\nexport function NavUser({\n  user,\n  onAction,\n}: {\n  user: AppSidebarUser;\n  onAction?: (action: AppSidebarUserAction) => void;\n}) {\n  const { isMobile } = useSidebar();\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenuTrigger>\n          <SidebarMenuButton\n            size=\"lg\"\n            aria-label={`Account: ${user.name}`}\n            className=\"aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground\"\n          >\n            <UserSummary user={user} />\n            <ChevronsUpDownIcon aria-hidden=\"true\" className=\"ml-auto size-4\" />\n          </SidebarMenuButton>\n          <DropdownMenu\n            placement={isMobile ? \"bottom end\" : \"end bottom\"}\n            className=\"w-fit min-w-56\"\n            onAction={(key) => onAction?.(key as AppSidebarUserAction)}\n          >\n            <DropdownMenuGroup>\n              <DropdownMenuLabel className=\"flex items-center gap-2 px-1 py-1.5 font-normal text-foreground\">\n                <UserSummary user={user} />\n              </DropdownMenuLabel>\n            </DropdownMenuGroup>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem id=\"upgrade\" textValue=\"Upgrade to Pro\">\n              <SparklesIcon aria-hidden=\"true\" />\n              Upgrade to Pro\n            </DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuGroup>\n              <DropdownMenuItem id=\"account\" textValue=\"Account\">\n                <BadgeCheckIcon aria-hidden=\"true\" />\n                Account\n              </DropdownMenuItem>\n              <DropdownMenuItem id=\"billing\" textValue=\"Billing\">\n                <CreditCardIcon aria-hidden=\"true\" />\n                Billing\n              </DropdownMenuItem>\n              <DropdownMenuItem id=\"notifications\" textValue=\"Notifications\">\n                <BellIcon aria-hidden=\"true\" />\n                Notifications\n              </DropdownMenuItem>\n            </DropdownMenuGroup>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem id=\"logout\" textValue=\"Log out\">\n              <LogOutIcon aria-hidden=\"true\" />\n              Log out\n            </DropdownMenuItem>\n          </DropdownMenu>\n        </DropdownMenuTrigger>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/fma-ui/app-sidebar/nav-user.tsx"
    },
    {
      "path": "src/blocks/app-sidebar/team-switcher.tsx",
      "content": "\"use client\";\n\nimport { ChevronsUpDownIcon, PlusIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuTrigger,\n} from \"@/core/dropdown-menu/dropdown-menu\";\nimport {\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  useSidebar,\n} from \"@/core/sidebar/sidebar\";\nimport type { AppSidebarLogo, AppSidebarTeam } from \"./types\";\n\nexport function TeamSwitcher({\n  teams,\n  logo,\n  onTeamChange,\n  onAddTeam,\n}: {\n  teams?: AppSidebarTeam[];\n  logo?: AppSidebarLogo;\n  onTeamChange?: (team: AppSidebarTeam) => void;\n  onAddTeam?: () => void;\n}) {\n  const { isMobile } = useSidebar();\n  const [activeTeam, setActiveTeam] = React.useState(teams?.[0]);\n\n  if (!teams?.length || !activeTeam) {\n    // Same footprint as the switcher button, so collapsing behaves alike.\n    return (\n      <SidebarMenu>\n        <SidebarMenuItem>\n          <div\n            data-slot=\"app-sidebar-logo\"\n            className=\"flex h-12 w-full items-center justify-center overflow-hidden rounded-md group-data-[collapsible=icon]:size-8!\"\n          >\n            <div className=\"flex size-full items-center justify-center group-data-[collapsible=icon]:hidden\">\n              {logo?.full}\n            </div>\n            <div className=\"hidden size-full items-center justify-center group-data-[collapsible=icon]:flex\">\n              {logo?.collapsed}\n            </div>\n          </div>\n        </SidebarMenuItem>\n      </SidebarMenu>\n    );\n  }\n\n  return (\n    <SidebarMenu>\n      <SidebarMenuItem>\n        <DropdownMenuTrigger>\n          <SidebarMenuButton\n            size=\"lg\"\n            aria-label={`Team: ${activeTeam.name}`}\n            className=\"aria-expanded:bg-sidebar-accent aria-expanded:text-sidebar-accent-foreground\"\n          >\n            <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n              {activeTeam.logo}\n            </div>\n            <div className=\"grid flex-1 text-left text-sm leading-tight\">\n              <span className=\"truncate font-medium\">{activeTeam.name}</span>\n              <span className=\"truncate text-xs\">{activeTeam.plan}</span>\n            </div>\n            <ChevronsUpDownIcon aria-hidden=\"true\" className=\"ml-auto\" />\n          </SidebarMenuButton>\n          <DropdownMenu\n            placement={isMobile ? \"bottom start\" : \"end top\"}\n            className=\"w-fit min-w-56\"\n            onAction={(key) => {\n              if (key === \"add-team\") return onAddTeam?.();\n              const team = teams.find((candidate) => candidate.name === key);\n              if (!team) return;\n              setActiveTeam(team);\n              onTeamChange?.(team);\n            }}\n          >\n            <DropdownMenuGroup>\n              <DropdownMenuLabel>Teams</DropdownMenuLabel>\n              {teams.map((team, index) => (\n                <DropdownMenuItem key={team.name} id={team.name} className=\"gap-2 p-2\">\n                  <div className=\"flex size-6 items-center justify-center rounded-md border\">\n                    {team.logo}\n                  </div>\n                  {team.name}\n                  <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>\n                </DropdownMenuItem>\n              ))}\n            </DropdownMenuGroup>\n            {onAddTeam && (\n              <>\n                <DropdownMenuSeparator />\n                <DropdownMenuItem id=\"add-team\" textValue=\"Add team\" className=\"gap-2 p-2\">\n                  <div className=\"flex size-6 items-center justify-center rounded-md border\">\n                    <PlusIcon aria-hidden=\"true\" className=\"size-4\" />\n                  </div>\n                  <span className=\"font-medium text-muted-foreground\">Add team</span>\n                </DropdownMenuItem>\n              </>\n            )}\n          </DropdownMenu>\n        </DropdownMenuTrigger>\n      </SidebarMenuItem>\n    </SidebarMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/fma-ui/app-sidebar/team-switcher.tsx"
    },
    {
      "path": "src/blocks/app-sidebar/types.ts",
      "content": "import type * as React from \"react\";\n\n/**\n * Data contract for the `app-sidebar` block: adding or removing a team,\n * section, item or sub-item is a change to this data, not to the block.\n */\n\nexport type AppSidebarUser = {\n  name: string;\n  email: string;\n  avatar: string;\n};\n\nexport type AppSidebarTeam = {\n  name: string;\n  logo: React.ReactNode;\n  plan: string;\n};\n\n/**\n * `full` shows while the sidebar is expanded (and on mobile); `collapsed`\n * while it's retracted to icons. Swapped with CSS only.\n */\nexport type AppSidebarLogo = {\n  full: React.ReactNode;\n  collapsed: React.ReactNode;\n};\n\n/** Either a team switcher or a static logo fills the header. */\nexport type AppSidebarBranding =\n  | { teams: AppSidebarTeam[]; logo?: never }\n  | { teams?: never; logo: AppSidebarLogo };\n\n/**\n * Links are react-aria `Link`s: wrap the app in react-aria's `RouterProvider`\n * to route them client-side.\n */\nexport type AppSidebarNavSubItem = {\n  title: string;\n  url: string;\n  icon?: React.ReactNode;\n};\n\n/**\n * `variant` picks how an item with sub-items opens: `\"collapse\"` (default)\n * expands them inline, `\"dropdown\"` opens a menu beside the item. While the\n * sidebar is collapsed to icons every item uses the dropdown.\n */\nexport type AppSidebarNavItem = {\n  title: string;\n  url: string;\n  icon?: React.ReactNode;\n  /** Marks the current page and starts a collapse item expanded. */\n  isActive?: boolean;\n  variant?: \"collapse\" | \"dropdown\";\n  items?: AppSidebarNavSubItem[];\n};\n\n/** A sidebar section, rendered as a `SidebarGroup`. */\nexport type AppSidebarNavSection = {\n  title: string;\n  items: AppSidebarNavItem[];\n};\n\nexport type AppSidebarData = {\n  user: AppSidebarUser;\n  nav: AppSidebarNavSection[];\n} & AppSidebarBranding;\n\n/** Ids of the entries in the user menu, passed to `onUserAction`. */\nexport type AppSidebarUserAction = \"upgrade\" | \"account\" | \"billing\" | \"notifications\" | \"logout\";\n",
      "type": "registry:component",
      "target": "components/fma-ui/app-sidebar/types.ts"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "origin": "shadcn/ui block sidebar-07 (via EMITTE), rebuilt on the registry's react-aria Sidebar, Collapsible and DropdownMenu",
    "tags": [
      "block",
      "sidebar",
      "navigation",
      "app-shell"
    ]
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}