{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:component",
  "title": "Combobox",
  "description": "Searchable select input for choosing from a list of options.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/combobox/combobox.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Check, ChevronsUpDown } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@vllnt/ui\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@vllnt/ui\";\n\nexport type ComboboxOption = {\n  disabled?: boolean;\n  keywords?: string[];\n  label: string;\n  value: string;\n};\n\nexport type ComboboxProps = {\n  className?: string;\n  commandClassName?: string;\n  emptyText?: string;\n  onValueChange?: (value: string) => void;\n  options: ComboboxOption[];\n  placeholder?: string;\n  searchPlaceholder?: string;\n  triggerClassName?: string;\n  value?: string;\n};\n\nfunction useComboboxValue(\n  value: string | undefined,\n  onValueChange?: (value: string) => void,\n) {\n  const [internalValue, setInternalValue] = React.useState(value ?? \"\");\n\n  React.useEffect(() => {\n    if (value !== undefined) {\n      setInternalValue(value);\n    }\n  }, [value]);\n\n  const resolvedValue = value ?? internalValue;\n\n  const setResolvedValue = (nextValue: string) => {\n    if (value === undefined) {\n      setInternalValue(nextValue);\n    }\n\n    onValueChange?.(nextValue);\n  };\n\n  return { resolvedValue, setResolvedValue };\n}\n\nfunction ComboboxOptionItem({\n  onSelect,\n  option,\n  selectedValue,\n}: {\n  onSelect: (value: string) => void;\n  option: ComboboxOption;\n  selectedValue: string;\n}) {\n  const keywords = option.keywords?.join(\" \") ?? \"\";\n\n  return (\n    <CommandItem\n      disabled={option.disabled}\n      onSelect={() => {\n        onSelect(option.value);\n      }}\n      value={`${option.label} ${option.value} ${keywords}`}\n    >\n      <Check\n        className={cn(\n          \"mr-2 size-4\",\n          selectedValue === option.value ? \"opacity-100\" : \"opacity-0\",\n        )}\n      />\n      <span className=\"truncate\">{option.label}</span>\n    </CommandItem>\n  );\n}\n\nfunction ComboboxListPanel({\n  className,\n  commandClassName,\n  emptyText,\n  listboxId,\n  onSelect,\n  options,\n  resolvedValue,\n  searchPlaceholder,\n}: {\n  className?: string;\n  commandClassName?: string;\n  emptyText: string;\n  listboxId: string;\n  onSelect: (value: string) => void;\n  options: ComboboxOption[];\n  resolvedValue: string;\n  searchPlaceholder: string;\n}) {\n  return (\n    <PopoverContent\n      className={cn(\"w-[var(--radix-popover-trigger-width)] p-0\", className)}\n    >\n      <Command className={commandClassName}>\n        <CommandInput placeholder={searchPlaceholder} />\n        <CommandList id={listboxId}>\n          <CommandEmpty>{emptyText}</CommandEmpty>\n          <CommandGroup>\n            {options.map((option) => (\n              <ComboboxOptionItem\n                key={option.value}\n                onSelect={onSelect}\n                option={option}\n                selectedValue={resolvedValue}\n              />\n            ))}\n          </CommandGroup>\n        </CommandList>\n      </Command>\n    </PopoverContent>\n  );\n}\n\nconst Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>(\n  (\n    {\n      className,\n      commandClassName,\n      emptyText = \"No option found.\",\n      onValueChange,\n      options,\n      placeholder = \"Select an option\",\n      searchPlaceholder = \"Search options...\",\n      triggerClassName,\n      value,\n    },\n    reference,\n  ) => {\n    const [open, setOpen] = React.useState(false);\n    const listboxId = React.useId();\n    const { resolvedValue, setResolvedValue } = useComboboxValue(\n      value,\n      onValueChange,\n    );\n    const selectedOption = options.find(\n      (option) => option.value === resolvedValue,\n    );\n\n    const handleSelect = (nextValue: string) => {\n      setResolvedValue(nextValue === resolvedValue ? \"\" : nextValue);\n      setOpen(false);\n    };\n\n    return (\n      <Popover onOpenChange={setOpen} open={open}>\n        <PopoverTrigger asChild>\n          <Button\n            aria-controls={listboxId}\n            aria-expanded={open}\n            aria-haspopup=\"listbox\"\n            className={cn(\"w-full justify-between\", triggerClassName)}\n            ref={reference}\n            role=\"combobox\"\n            variant=\"outline\"\n          >\n            <span className=\"truncate\">\n              {selectedOption ? selectedOption.label : placeholder}\n            </span>\n            <ChevronsUpDown className=\"ml-2 size-4 shrink-0 opacity-50\" />\n          </Button>\n        </PopoverTrigger>\n        <ComboboxListPanel\n          className={className}\n          commandClassName={commandClassName}\n          emptyText={emptyText}\n          listboxId={listboxId}\n          onSelect={handleSelect}\n          options={options}\n          resolvedValue={resolvedValue}\n          searchPlaceholder={searchPlaceholder}\n        />\n      </Popover>\n    );\n  },\n);\nCombobox.displayName = \"Combobox\";\n\nexport { Combobox };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable",
  "a11y": {
    "role": "combobox",
    "keyboard": [
      {
        "keys": "Enter / Space",
        "action": "open the listbox"
      },
      {
        "keys": "ArrowDown",
        "action": "open + move active option down"
      },
      {
        "keys": "ArrowUp",
        "action": "move active option up"
      },
      {
        "keys": "Escape",
        "action": "close the listbox"
      },
      {
        "keys": "Type-ahead",
        "action": "filter options"
      }
    ],
    "aria": [
      "aria-controls",
      "aria-expanded",
      "aria-haspopup"
    ],
    "focusManagement": "auto",
    "notes": "Trigger button has role='combobox'. Listbox id is wired via React.useId() so consumers don't need to manage it."
  },
  "examples": [
    {
      "title": "Default",
      "description": "Single-select combobox with type-ahead.",
      "code": "import { Combobox } from \"@vllnt/ui\";\n\nconst options = [\n  { label: \"React\", value: \"react\" },\n  { label: \"Vue\", value: \"vue\" },\n  { label: \"Svelte\", value: \"svelte\" },\n  { label: \"Solid\", value: \"solid\" },\n];\n\nexport function Demo() {\n  return <Combobox options={options} placeholder=\"Pick a framework\" />;\n}\n",
      "framework": "react"
    },
    {
      "title": "Controlled",
      "description": "Pair Combobox with React.useState for full control over the selected value.",
      "code": "\"use client\";\nimport * as React from \"react\";\nimport { Combobox } from \"@vllnt/ui\";\n\nconst options = [\n  { label: \"Pending\", value: \"pending\" },\n  { label: \"Approved\", value: \"approved\" },\n  { label: \"Rejected\", value: \"rejected\" },\n];\n\nexport function Demo() {\n  const [value, setValue] = React.useState(\"pending\");\n  return (\n    <Combobox\n      options={options}\n      value={value}\n      onValueChange={setValue}\n      placeholder=\"Status\"\n    />\n  );\n}\n",
      "framework": "react"
    }
  ],
  "props": [
    {
      "name": "options",
      "type": "ComboboxOption[]",
      "required": true,
      "description": "List of options. Each option must have a unique `value`. Optional `keywords` are added to the searchable haystack."
    },
    {
      "name": "value",
      "type": "string",
      "required": false,
      "description": "Controlled selected value. Pair with `onValueChange`. Omit for uncontrolled."
    },
    {
      "name": "onValueChange",
      "type": "(value: string) => void",
      "required": false,
      "description": "Called when the user picks an option (or clears by re-selecting the active one)."
    },
    {
      "name": "placeholder",
      "type": "string",
      "required": false,
      "defaultValue": "\"Select an option\"",
      "description": "Trigger label when no option is selected."
    },
    {
      "name": "searchPlaceholder",
      "type": "string",
      "required": false,
      "defaultValue": "\"Search options...\"",
      "description": "Placeholder for the type-ahead search input inside the listbox."
    },
    {
      "name": "emptyText",
      "type": "string",
      "required": false,
      "defaultValue": "\"No option found.\"",
      "description": "Shown when the search yields zero matches."
    },
    {
      "name": "className",
      "type": "string",
      "required": false,
      "description": "Class applied to the popover content (the listbox container)."
    },
    {
      "name": "triggerClassName",
      "type": "string",
      "required": false,
      "description": "Class applied to the trigger button."
    },
    {
      "name": "commandClassName",
      "type": "string",
      "required": false,
      "description": "Class applied to the inner Command element."
    }
  ]
}
