{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:ui",
  "title": "Combobox",
  "description": "A searchable single-select field built on Popover and Command.",
  "dependencies": [
    "@tabler/icons-react"
  ],
  "registryDependencies": [
    "https://abackui.com/r/utils.json",
    "https://abackui.com/r/command.json",
    "https://abackui.com/r/popover.json"
  ],
  "categories": [
    "Forms"
  ],
  "docs": "https://abackui.com/docs/components/combobox",
  "meta": {
    "status": "beta"
  },
  "files": [
    {
      "path": "components/ui/combobox.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconCheck, IconChevronDown } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\n\nexport interface ComboboxOption {\n  value: string;\n  label: string;\n  /** Secondary line shown under the label in the list. */\n  description?: string;\n  disabled?: boolean;\n  /** Extra words that should match the search without appearing as the label. */\n  keywords?: string[];\n}\n\nexport interface ComboboxProps {\n  options: ComboboxOption[];\n  value?: string;\n  onValueChange?: (value: string) => void;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  emptyMessage?: string;\n  disabled?: boolean;\n  /** Accessible name for the trigger when there is no visible `Label`. */\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: boolean;\n  id?: string;\n  name?: string;\n  className?: string;\n  triggerClassName?: string;\n  align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * A single-select combobox: a button that opens a searchable list.\n *\n * Built from `Popover` and `Command`, so filtering, type-ahead, arrow keys,\n * Home/End, Enter, and Escape are real interactions rather than decoration.\n * The trigger keeps the current label, and the selected item is marked with a\n * check icon and `aria-selected` from the listbox.\n */\nfunction Combobox({\n  options,\n  value,\n  onValueChange,\n  placeholder = \"Select an option\",\n  searchPlaceholder = \"Search…\",\n  emptyMessage = \"No matching options.\",\n  disabled,\n  id,\n  name,\n  className,\n  triggerClassName,\n  align = \"start\",\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n}: ComboboxProps) {\n  const [open, setOpen] = React.useState(false);\n  const selected = options.find((option) => option.value === value);\n  const generatedId = React.useId();\n  const triggerId = id ?? generatedId;\n\n  return (\n    <div data-slot=\"combobox\" className={cn(\"relative w-full\", className)}>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <button\n            id={triggerId}\n            type=\"button\"\n            aria-expanded={open}\n            aria-haspopup=\"listbox\"\n            aria-controls={open ? `${triggerId}-listbox` : undefined}\n            aria-label={ariaLabel}\n            aria-labelledby={ariaLabelledBy}\n            aria-describedby={ariaDescribedBy}\n            aria-invalid={ariaInvalid}\n            disabled={disabled}\n            className={cn(\n              \"flex h-9 w-full items-center justify-between gap-2 border border-input bg-background px-3 text-left text-sm text-foreground shadow-xs\",\n              \"outline-none transition-[color,background-color,border-color,box-shadow] duration-200\",\n              \"hover:bg-muted/40\",\n              \"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n              \"disabled:cursor-not-allowed disabled:opacity-50\",\n              \"aria-invalid:border-destructive aria-invalid:ring-destructive/20\",\n              triggerClassName,\n            )}\n          >\n            <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n              {selected?.label ?? placeholder}\n            </span>\n            <IconChevronDown\n              aria-hidden=\"true\"\n              className={cn(\n                \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n                open && \"rotate-180\",\n              )}\n            />\n          </button>\n        </PopoverTrigger>\n        <PopoverContent\n          align={align}\n          aria-label={ariaLabel ?? \"Choose an option\"}\n          className=\"w-[var(--radix-popover-trigger-width)] min-w-56 p-0\"\n        >\n          <Command label={ariaLabel ?? \"Options\"}>\n            <CommandInput placeholder={searchPlaceholder} autoFocus />\n            <CommandList id={`${triggerId}-listbox`}>\n              <CommandEmpty>{emptyMessage}</CommandEmpty>\n              {options.map((option) => (\n                <CommandItem\n                  key={option.value}\n                  value={option.value}\n                  keywords={[option.label, ...(option.keywords ?? [])]}\n                  disabled={option.disabled}\n                  onSelect={() => {\n                    onValueChange?.(option.value);\n                    setOpen(false);\n                  }}\n                  className=\"items-start gap-2\"\n                >\n                  <IconCheck\n                    aria-hidden=\"true\"\n                    className={cn(\n                      \"mt-0.5 size-4 shrink-0\",\n                      option.value === value ? \"opacity-100\" : \"opacity-0\",\n                    )}\n                  />\n                  <span className=\"flex min-w-0 flex-col gap-0.5\">\n                    <span className=\"truncate\">{option.label}</span>\n                    {option.description ? (\n                      <span className=\"truncate text-xs text-muted-foreground\">\n                        {option.description}\n                      </span>\n                    ) : null}\n                  </span>\n                </CommandItem>\n              ))}\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n      {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n    </div>\n  );\n}\n\nexport { Combobox };\n",
      "type": "registry:ui",
      "target": "components/ui/combobox.tsx"
    }
  ]
}
