{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "user-menu",
  "title": "User Menu",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An account menu on the dropdown: avatar or avatar-with-name trigger, a header card with plan, link groups with shortcuts and badges, a theme submenu and sign out.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/dropdown-menu.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/user-menu.tsx",
      "content": "// Ballmac UI: User Menu. https://ui.ballmac.com/components/user-menu\n\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronsUpDown, LogOut, Monitor, Moon, Palette, Sun } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage, type AvatarProps } from \"@/components/ballmac/avatar\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ballmac/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages, defineMessage } from \"@/lib/ballmac/i18n\";\n\ntype UserMenuUser = {\n  name: string;\n  email: string;\n  /** Avatar image URL. Initials show while it loads or if it fails. */\n  image?: string;\n  /** Plan label shown as a badge in the menu header, for example \"Pro\". */\n  plan?: string;\n  /** Presence dot on the avatar. */\n  status?: AvatarProps[\"status\"];\n};\n\ntype UserMenuItem = {\n  label: string;\n  /** Navigates when set. */\n  href?: string;\n  /** Runs when chosen. */\n  onSelect?: () => void;\n  icon?: React.ReactNode;\n  /** Keys shown at the end. Display only. */\n  shortcut?: string;\n  /** Small text at the end, for example \"3 new\". */\n  badge?: string;\n};\n\ntype UserMenuTheme = \"light\" | \"dark\" | \"system\";\n\ntype UserMenuProps = Omit<React.ComponentProps<\"button\">, \"children\"> & {\n  user: UserMenuUser;\n  /** Groups of items, shown in order with dividers between them. */\n  groups?: UserMenuItem[][];\n  /** Current theme. Adds a Theme submenu when `onThemeChange` is also set. */\n  theme?: UserMenuTheme;\n  /** Called with the chosen theme. */\n  onThemeChange?: (theme: UserMenuTheme) => void;\n  /** Adds a destructive \"Sign out\" item at the end. */\n  onSignOut?: () => void;\n  /** Trigger style: just the avatar, or avatar with name and email (for sidebars). */\n  variant?: \"avatar\" | \"full\";\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  /** Open state, for controlled use. */\n  open?: boolean;\n  /** Initial open state. */\n  defaultOpen?: boolean;\n  /** Called when the menu opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Trap interaction inside the menu while open. Turn off to keep the page usable, for example in a docs preview. */\n  modal?: boolean;\n};\n\nfunction initials(name: string) {\n  return name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((p) => p.charAt(0).toUpperCase())\n    .join(\"\");\n}\n\nfunction UserAvatar({ user, size }: { user: UserMenuUser; size?: AvatarProps[\"size\"] }) {\n  return (\n    <Avatar size={size} status={user.status}>\n      {user.image && <AvatarImage src={user.image} alt=\"\" />}\n      <AvatarFallback>{initials(user.name)}</AvatarFallback>\n    </Avatar>\n  );\n}\n\nconst THEMES = [\n  { value: \"light\", label: defineMessage(\"user-menu.THEMES.0\", \"Light\"), icon: Sun },\n  { value: \"dark\", label: defineMessage(\"user-menu.THEMES.1\", \"Dark\"), icon: Moon },\n  { value: \"system\", label: defineMessage(\"user-menu.THEMES.2\", \"System\"), icon: Monitor },\n] as const;\n\n/**\n * An account menu: avatar (or avatar with name) opens a card-style header, your links, an optional theme picker and sign out.\n * Built on the dropdown menu, so arrow keys, typeahead and Escape work, and the avatar's alt text is the person's name.\n */\nfunction UserMenu({\n  user,\n  groups = [],\n  theme,\n  onThemeChange,\n  onSignOut,\n  variant = \"avatar\",\n  align = \"end\",\n  side = \"bottom\",\n  open,\n  defaultOpen,\n  onOpenChange,\n  modal = true,\n  className,\n  ...props\n}: UserMenuProps) {\n  const msg = useMessages()\n  return (\n    <DropdownMenu open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange} modal={modal}>\n      <DropdownMenuTrigger\n        data-slot=\"user-menu\"\n        aria-label={variant === \"avatar\" ? `Account menu for ${user.name}` : undefined}\n        className={cn(\n          variant === \"avatar\"\n            ? \"rounded-full outline-none transition-shadow hover:ring-4 hover:ring-ring/15 focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=open]:ring-4 data-[state=open]:ring-ring/20\"\n            : \"flex w-full min-w-0 items-center gap-3 rounded-xl border bg-background p-2 pe-3 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          className,\n        )}\n        {...props}\n      >\n        <UserAvatar user={user} />\n        {variant === \"full\" && (\n          <>\n            <span className=\"grid min-w-0 flex-1 leading-tight\">\n              <span className=\"truncate text-sm font-semibold\">{user.name}</span>\n              <span className=\"truncate text-xs text-muted-foreground\">{user.email}</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-72 p-0\">\n        <div className=\"flex items-center gap-3 border-b bg-gradient-to-br from-chart-1/[0.08] to-transparent p-4\">\n          <UserAvatar user={user} size=\"lg\" />\n          <div className=\"grid min-w-0 flex-1 leading-tight\">\n            <span className=\"truncate text-sm font-semibold\">{user.name}</span>\n            <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span>\n          </div>\n          {user.plan && (\n            <span className=\"shrink-0 rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold tracking-wide text-primary-foreground uppercase\">\n              {user.plan}\n            </span>\n          )}\n        </div>\n        <div className=\"p-1\">\n          {groups.map((group, gi) => (\n            <React.Fragment key={gi}>\n              {gi > 0 && <DropdownMenuSeparator />}\n              <DropdownMenuGroup>\n                {group.map((item) => {\n                  const inner = (\n                    <>\n                      {item.icon}\n                      {item.label}\n                      {item.badge && (\n                        <span className=\"ms-auto rounded-full bg-muted px-1.5 py-px text-[10px] font-semibold text-muted-foreground\">{item.badge}</span>\n                      )}\n                      {item.shortcut && <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>}\n                    </>\n                  );\n                  // createElement keeps `asChild` out of JSX (the shadcn CLI rewrites it for Base UI projects and would break this\n                  // Radix-based menu). The item renders as the link itself, so there is one interactive element, not two.\n                  return item.href ? (\n                    React.createElement(\n                      DropdownMenuItem,\n                      { key: item.label, asChild: true, onSelect: item.onSelect },\n                      <a href={item.href}>{inner}</a>,\n                    )\n                  ) : (\n                    <DropdownMenuItem key={item.label} onSelect={item.onSelect}>\n                      {inner}\n                    </DropdownMenuItem>\n                  );\n                })}\n              </DropdownMenuGroup>\n            </React.Fragment>\n          ))}\n          {theme && onThemeChange && (\n            <>\n              {groups.length > 0 && <DropdownMenuSeparator />}\n              <DropdownMenuSub>\n                <DropdownMenuSubTrigger>\n                  <Palette aria-hidden=\"true\" />\n                  Theme\n                  <span className=\"ms-auto pe-1 text-xs text-muted-foreground capitalize\">{theme}</span>\n                </DropdownMenuSubTrigger>\n                <DropdownMenuSubContent>\n                  <DropdownMenuRadioGroup value={theme} onValueChange={(v) => onThemeChange(v as UserMenuTheme)}>\n                    {THEMES.map(({ value, label, icon: Icon }) => (\n                      <DropdownMenuRadioItem key={value} value={value}>\n                        <Icon aria-hidden=\"true\" />\n                        {msg.of(label)}\n                      </DropdownMenuRadioItem>\n                    ))}\n                  </DropdownMenuRadioGroup>\n                </DropdownMenuSubContent>\n              </DropdownMenuSub>\n            </>\n          )}\n          {onSignOut && (\n            <>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem destructive onSelect={onSignOut}>\n                <LogOut aria-hidden=\"true\" />\n                {msg(\"user-menu.signOut\", \"Sign out\")}\n              </DropdownMenuItem>\n            </>\n          )}\n        </div>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nexport { UserMenu, type UserMenuProps, type UserMenuUser, type UserMenuItem, type UserMenuTheme };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/user-menu.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "account",
      "avatar",
      "menu",
      "theme"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "user {name,email,image,plan,status}, groups: UserMenuItem[][], theme/onThemeChange, onSignOut, variant avatar | full.",
      "whenToUse": [
        "Headers and sidebar footers",
        "Account access with theme and sign out"
      ],
      "whenNotToUse": [
        "Switching workspaces; use team-switcher",
        "Generic action menus; use dropdown-menu"
      ],
      "composesWith": [
        "avatar",
        "dropdown-menu",
        "team-switcher"
      ],
      "a11y": [
        {
          "keys": "Enter / Space / ArrowDown",
          "action": "Opens the menu"
        },
        {
          "keys": "Arrow keys and typeahead",
          "action": "Move between items"
        },
        {
          "keys": "Escape",
          "action": "Closes and returns focus"
        },
        {
          "keys": "Screen readers",
          "action": "The avatar button names the person; links are real links"
        }
      ],
      "customization": [
        "variant",
        "groups with icons, shortcuts and badges",
        "theme submenu",
        "open state props"
      ]
    },
    "examples": [
      "user-menu-demo",
      "user-menu-states"
    ],
    "url": "https://ui.ballmac.com/components/user-menu"
  },
  "categories": [
    "saas"
  ],
  "type": "registry:ui"
}