{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "title": "Combobox",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/command.json",
    "https://ui.ballmac.com/r/popover.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/combobox.tsx",
      "content": "// Ballmac UI: Combobox. https://ui.ballmac.com/components/combobox\n\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronsUpDown, X } from \"lucide-react\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ballmac/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ballmac/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype ComboboxOption = {\n  /** Unique value returned in `onValueChange`. */\n  value: string;\n  /** Text shown in the list and in the trigger. */\n  label: string;\n  /** Muted second line in the list. */\n  description?: string;\n  /** Extra words the search should match. */\n  keywords?: string[];\n  /** Leading icon or avatar. */\n  icon?: React.ReactNode;\n  /** Prevent choosing this option. */\n  disabled?: boolean;\n  /** Optional group heading; options with the same group are listed together. */\n  group?: string;\n};\n\ntype ComboboxProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n  /** Options to choose from. */\n  options: ComboboxOption[];\n  /** Controlled selected value (\"\" for none). */\n  value?: string;\n  /** Initial value when uncontrolled. */\n  defaultValue?: string;\n  /** Called with the new value, or \"\" when cleared. */\n  onValueChange?: (value: string) => void;\n  /** Text on the trigger when nothing is chosen. */\n  placeholder?: string;\n  /** Placeholder inside the search field. */\n  searchPlaceholder?: string;\n  /** Text shown when the search has no matches. */\n  emptyText?: string;\n  /** Show a control that clears the selection. */\n  clearable?: boolean;\n  /** Mark the field invalid (`aria-invalid`, destructive border). */\n  invalid?: boolean;\n  /** Name for a hidden input, so the value submits with a native form. */\n  name?: string;\n  /** Classes for the popup panel. */\n  contentClassName?: string;\n};\n\n/** A searchable single-choice picker: a trigger button that opens a filterable list. */\nfunction Combobox({\n  options,\n  value: valueProp,\n  defaultValue = \"\",\n  onValueChange,\n  placeholder,\n  searchPlaceholder,\n  emptyText,\n  clearable = false,\n  invalid = false,\n  disabled,\n  name,\n  className,\n  contentClassName,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ComboboxProps) {\n  const msg = useMessages()\n  placeholder ??= msg(\"combobox.placeholder\", \"Select an option\")\n  searchPlaceholder ??= msg(\"combobox.searchPlaceholder\", \"Search\")\n  emptyText ??= msg(\"combobox.emptyText\", \"No results found.\")\n  const [open, setOpen] = React.useState(false);\n  const [inner, setInner] = React.useState(defaultValue);\n  const value = valueProp ?? inner;\n  const selected = options.find((o) => o.value === value);\n  const choose = (next: string) => {\n    if (valueProp === undefined) setInner(next);\n    onValueChange?.(next);\n  };\n  const groups = React.useMemo(() => {\n    const map = new Map<string, ComboboxOption[]>();\n    for (const o of options) {\n      const key = o.group ?? \"\";\n      map.set(key, [...(map.get(key) ?? []), o]);\n    }\n    return [...map.entries()];\n  }, [options]);\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <div className=\"relative w-full\">\n        <PopoverTrigger\n          data-slot=\"combobox\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-invalid={invalid || undefined}\n          aria-label={ariaLabel}\n          disabled={disabled}\n          className={cn(\n            \"flex h-9 w-full min-w-0 items-center justify-between gap-2 rounded-md border border-input bg-background px-3 text-sm shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 hover:bg-accent/40 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30\",\n            clearable && selected && \"pe-14\",\n            !selected && \"text-muted-foreground\",\n            className,\n          )}\n          {...props}\n        >\n          <span className=\"flex min-w-0 items-center gap-2\">\n            {selected?.icon}\n            <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n          </span>\n          <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        </PopoverTrigger>\n        {clearable && selected && !disabled && (\n          <button\n            type=\"button\"\n            aria-label={msg(\"combobox.clearSelection\", \"Clear selection\")}\n            onClick={() => choose(\"\")}\n            className=\"absolute top-1/2 end-8 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n          >\n            <X aria-hidden=\"true\" className=\"size-3.5\" />\n          </button>\n        )}\n        {name && <input type=\"hidden\" name={name} value={value} />}\n      </div>\n      <PopoverContent\n        label={ariaLabel ?? placeholder}\n        align=\"start\"\n        sideOffset={4}\n        className={cn(\n          \"w-(--radix-popover-trigger-width) min-w-56 gap-0 p-0\",\n          contentClassName,\n        )}\n      >\n        <Command label={ariaLabel ?? placeholder}>\n          <CommandInput placeholder={searchPlaceholder} />\n          <CommandList>\n            <CommandEmpty>{emptyText}</CommandEmpty>\n            {groups.map(([heading, list]) => (\n              <CommandGroup key={heading || \"options\"} heading={heading || undefined}>\n                {list.map((o) => (\n                  <CommandItem\n                    key={o.value}\n                    value={o.value}\n                    keywords={[o.label, ...(o.keywords ?? [])]}\n                    icon={o.icon}\n                    description={o.description}\n                    selected={o.value === value}\n                    disabled={o.disabled}\n                    onSelect={() => {\n                      choose(o.value === value && clearable ? \"\" : o.value);\n                      setOpen(false);\n                    }}\n                  >\n                    {o.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            ))}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport { Combobox, type ComboboxProps, type ComboboxOption };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/combobox.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "select",
      "search",
      "autocomplete",
      "form"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "Pass options with value and label. It opens a filterable list; the trigger is a combobox button that submits through name.",
      "whenToUse": [
        "Choosing from more than about seven options",
        "Time zones, countries, users and frameworks"
      ],
      "whenNotToUse": [
        "Short lists; use select or radio-group",
        "Choosing several values; use multi-select"
      ],
      "composesWith": [
        "field",
        "command",
        "popover"
      ],
      "a11y": [
        {
          "keys": "Enter / Space / ArrowDown",
          "action": "Opens the list from the trigger"
        },
        {
          "keys": "Type",
          "action": "Filters options; matches value, label and keywords"
        },
        {
          "keys": "ArrowUp / ArrowDown / Enter",
          "action": "Moves and selects"
        },
        {
          "keys": "Escape",
          "action": "Closes and returns focus to the trigger"
        }
      ],
      "customization": [
        "options with group, icon, description and keywords",
        "clearable",
        "invalid",
        "name for native form submission"
      ]
    },
    "examples": [
      "combobox-demo",
      "combobox-states"
    ],
    "url": "https://ui.ballmac.com/components/combobox"
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}