{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-switcher",
  "title": "Team Switcher",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A workspace picker: the current team on a button, the others in a radio menu with logos and descriptions, an add-team row, and a compact icon mode.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/dropdown-menu.json",
    "https://ui.ballmac.com/r/i18n.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/team-switcher.tsx",
      "content": "// Ballmac UI: Team Switcher. https://ui.ballmac.com/components/team-switcher\n\"use client\";\n\nimport * as React from \"react\";\nimport { Check, ChevronsUpDown, Plus } from \"lucide-react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ballmac/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\nimport { Media, isMediaImage, type MediaSource } from \"@/components/ballmac/media\";\n\ntype Team = {\n  /** Unique id; used for `value`. */\n  id: string;\n  /** Team or workspace name. */\n  name: string;\n  /** Secondary line, such as the plan or member count. */\n  description?: string;\n  /** Logo or avatar: an image URL, an object with alt text and a dark-mode file, or any element. Defaults to the first letter of the name. */\n  logo?: MediaSource;\n};\n\ntype TeamSwitcherProps = Omit<React.ComponentProps<\"button\">, \"value\" | \"defaultValue\" | \"onChange\"> & {\n  /** Teams to choose from. */\n  teams: Team[];\n  /** Controlled selected team id. */\n  value?: string;\n  /** Initial team id when uncontrolled. */\n  defaultValue?: string;\n  /** Called with the chosen team id. */\n  onValueChange?: (id: string) => void;\n  /** Shows an \"Add team\" row at the end of the menu and calls this when chosen. */\n  onAddTeam?: () => void;\n  /** Label of the add row. */\n  addLabel?: string;\n  /** Show only the logo (for a collapsed sidebar). The name stays available to screen readers. */\n  compact?: boolean;\n  /** Menu alignment relative to the trigger. */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Side of the trigger the menu opens on. */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n};\n\nfunction Logo({ team, className }: { team: Team; className?: string }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary text-sm font-semibold text-primary-foreground [&_svg]:size-4\",\n        className,\n      )}\n    >\n      {isMediaImage(team.logo) ? <Media media={team.logo} alt=\"\" fit=\"contain\" fill className=\"size-full\" /> : (team.logo ?? team.name.charAt(0).toUpperCase())}\n    </span>\n  );\n}\n\n/** A workspace picker: the current team on a button, the others in a radio menu, and an optional add row. */\nfunction TeamSwitcher({\n  teams,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  onAddTeam,\n  addLabel,\n  compact = false,\n  align = \"start\",\n  side = \"bottom\",\n  className,\n  ...props\n}: TeamSwitcherProps) {\n  const msg = useMessages()\n  addLabel ??= msg(\"team-switcher.addLabel\", \"Add team\")\n  const [inner, setInner] = React.useState(defaultValue ?? teams[0]?.id);\n  const value = valueProp ?? inner;\n  const active = teams.find((t) => t.id === value) ?? teams[0];\n  if (!active) return null;\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger\n        data-slot=\"team-switcher\"\n        aria-label={compact ? `Team: ${active.name}` : undefined}\n        className={cn(\n          \"flex w-full min-w-0 items-center gap-2.5 rounded-lg border bg-background p-1.5 pe-2 text-start shadow-xs outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=open]:bg-accent\",\n          compact && \"size-11 justify-center p-1.5 pe-1.5\",\n          className,\n        )}\n        {...props}\n      >\n        <Logo team={active} />\n        {!compact && (\n          <>\n            <span className=\"grid min-w-0 flex-1 leading-tight\">\n              <span className=\"truncate text-sm font-semibold\">{active.name}</span>\n              {active.description && <span className=\"truncate text-xs text-muted-foreground\">{active.description}</span>}\n            </span>\n            <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n          </>\n        )}\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align={align} side={side} className=\"w-(--radix-dropdown-menu-trigger-width) min-w-60\">\n        <DropdownMenuLabel>{msg(\"team-switcher.teams\", \"Teams\")}</DropdownMenuLabel>\n        <DropdownMenuRadioGroup\n          value={value}\n          onValueChange={(id) => {\n            if (valueProp === undefined) setInner(id);\n            onValueChange?.(id);\n          }}\n        >\n          {teams.map((team) => (\n            <DropdownMenuRadioItem key={team.id} value={team.id} className=\"gap-2.5 py-1.5 ps-2 [&>span:first-child]:hidden\">\n              <Logo team={team} className=\"size-7 rounded-md text-xs\" />\n              <span className=\"grid min-w-0 flex-1 leading-tight\">\n                <span className=\"truncate text-sm\">{team.name}</span>\n                {team.description && <span className=\"truncate text-xs text-muted-foreground\">{team.description}</span>}\n              </span>\n              {team.id === value && <Check aria-hidden=\"true\" className=\"ms-auto size-4 !text-foreground\" />}\n            </DropdownMenuRadioItem>\n          ))}\n        </DropdownMenuRadioGroup>\n        {onAddTeam && (\n          <>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem onSelect={onAddTeam} className=\"gap-2.5 py-1.5\">\n              <span className=\"flex size-7 items-center justify-center rounded-md border border-dashed text-muted-foreground\">\n                <Plus aria-hidden=\"true\" className=\"size-4\" />\n              </span>\n              {addLabel}\n            </DropdownMenuItem>\n          </>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport { TeamSwitcher, type TeamSwitcherProps, type Team };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/team-switcher.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "workspace",
      "switcher",
      "menu",
      "dropdown"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "teams: {id,name,description,logo}. value/defaultValue/onValueChange choose the active team; onAddTeam adds the last row; compact shows only the logo.",
      "whenToUse": [
        "Multi-workspace apps in a sidebar or header",
        "Switching organizations or projects"
      ],
      "whenNotToUse": [
        "Choosing a value in a form; use select or combobox",
        "Long searchable lists; use combobox"
      ],
      "composesWith": [
        "sidebar",
        "app-shell",
        "avatar"
      ],
      "a11y": [
        {
          "keys": "Enter / Space / ArrowDown",
          "action": "Opens the menu"
        },
        {
          "keys": "ArrowUp / ArrowDown",
          "action": "Moves between teams"
        },
        {
          "keys": "Enter",
          "action": "Chooses the team"
        },
        {
          "keys": "Escape",
          "action": "Closes and returns focus"
        }
      ],
      "customization": [
        "compact",
        "logo per team",
        "onAddTeam and addLabel",
        "side and align"
      ]
    },
    "examples": [
      "team-switcher-demo",
      "team-switcher-states"
    ],
    "url": "https://ui.ballmac.com/components/team-switcher"
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}