{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox",
  "type": "registry:ui",
  "title": "Combobox",
  "description": "Searchable autocomplete select with keyboard navigation, empty states, and badge support.",
  "registryDependencies": [
    "https://ui.arpitbhayani.me/r/aui.json"
  ],
  "files": [
    {
      "path": "ui/combobox.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ComboboxOption<T = string> {\n  value: T;\n  label: string;\n  description?: string;\n  badge?: string;\n  icon?: React.ReactNode;\n  disabled?: boolean;\n}\n\nexport interface ComboboxProps<T = string> {\n  options: ComboboxOption<T>[];\n  value?: T;\n  onChange: (value: T) => void;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  emptyText?: string;\n  loading?: boolean;\n  disabled?: boolean;\n  onSearchChange?: (query: string) => void;\n  renderOption?: (option: ComboboxOption<T>) => React.ReactNode;\n  className?: string;\n}\n\nexport function Combobox<T = string>({\n  options = [],\n  value,\n  onChange,\n  placeholder = \"Select option...\",\n  searchPlaceholder = \"Search...\",\n  emptyText = \"No options found\",\n  loading = false,\n  disabled = false,\n  onSearchChange,\n  renderOption,\n  className,\n}: ComboboxProps<T>) {\n  const [isOpen, setIsOpen] = React.useState(false);\n  const [search, setSearch] = React.useState(\"\");\n  const [highlightedIndex, setHighlightedIndex] = React.useState(0);\n\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const selectedOption = React.useMemo(\n    () => options.find((opt) => opt.value === value),\n    [options, value]\n  );\n\n  const filteredOptions = React.useMemo(() => {\n    if (onSearchChange) return options;\n    if (!search.trim()) return options;\n    const q = search.toLowerCase();\n    return options.filter(\n      (opt) =>\n        opt.label.toLowerCase().includes(q) ||\n        (opt.description && opt.description.toLowerCase().includes(q)) ||\n        (opt.badge && opt.badge.toLowerCase().includes(q))\n    );\n  }, [options, search, onSearchChange]);\n\n  React.useEffect(() => {\n    if (!isOpen) return;\n    const handleClickOutside = (e: MouseEvent) => {\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(e.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [isOpen]);\n\n  React.useEffect(() => {\n    if (isOpen) {\n      setHighlightedIndex(0);\n      const timer = setTimeout(() => {\n        inputRef.current?.focus();\n      }, 50);\n      return () => clearTimeout(timer);\n    } else {\n      setSearch(\"\");\n    }\n  }, [isOpen]);\n\n  const handleSelect = (option: ComboboxOption<T>) => {\n    if (option.disabled) return;\n    onChange(option.value);\n    setIsOpen(false);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (!isOpen) {\n      if (e.key === \"Enter\" || e.key === \" \" || e.key === \"ArrowDown\") {\n        e.preventDefault();\n        setIsOpen(true);\n      }\n      return;\n    }\n\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      setIsOpen(false);\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setHighlightedIndex((prev) =>\n        prev < filteredOptions.length - 1 ? prev + 1 : prev\n      );\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0));\n    } else if (e.key === \"Enter\") {\n      e.preventDefault();\n      if (filteredOptions[highlightedIndex]) {\n        handleSelect(filteredOptions[highlightedIndex]);\n      }\n    }\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative w-full\", className)}\n      onKeyDown={handleKeyDown}\n    >\n      <button\n        type=\"button\"\n        className={cn(\n          \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 text-left\",\n          isOpen && \"ring-1 ring-ring border-primary\"\n        )}\n        onClick={() => !disabled && setIsOpen((prev) => !prev)}\n        disabled={disabled}\n      >\n        <span className=\"truncate\">\n          {selectedOption ? (\n            <span className=\"inline-flex items-center gap-2\">\n              {selectedOption.icon}\n              <span className=\"font-medium text-foreground\">{selectedOption.label}</span>\n              {selectedOption.badge && (\n                <span className=\"font-mono text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground\">\n                  {selectedOption.badge}\n                </span>\n              )}\n            </span>\n          ) : (\n            <span className=\"text-muted-foreground\">{placeholder}</span>\n          )}\n        </span>\n        <svg\n          className={cn(\"h-4 w-4 shrink-0 opacity-50 transition-transform\", isOpen && \"rotate-180\")}\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n        >\n          <polyline points=\"6 9 12 15 18 9\" />\n        </svg>\n      </button>\n\n      {isOpen && (\n        <div className=\"absolute top-[calc(100%+4px)] left-0 right-0 z-50 rounded-md border border-border bg-popover text-popover-foreground shadow-lg overflow-hidden animate-in fade-in-0 zoom-in-95\">\n          <div className=\"flex items-center px-3 border-b border-border bg-muted/30\">\n            <svg\n              className=\"mr-2 h-4 w-4 shrink-0 opacity-50\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n            >\n              <circle cx=\"11\" cy=\"11\" r=\"8\" />\n              <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n            </svg>\n            <input\n              ref={inputRef}\n              type=\"text\"\n              className=\"flex h-9 w-full bg-transparent py-2 text-sm outline-hidden placeholder:text-muted-foreground\"\n              placeholder={searchPlaceholder}\n              value={search}\n              onChange={(e) => {\n                setSearch(e.target.value);\n                setHighlightedIndex(0);\n                onSearchChange?.(e.target.value);\n              }}\n            />\n          </div>\n\n          <ul className=\"max-h-[280px] overflow-y-auto p-1 text-sm\">\n            {loading ? (\n              <li className=\"py-6 text-center text-xs text-muted-foreground\">Loading...</li>\n            ) : filteredOptions.length === 0 ? (\n              <li className=\"py-6 text-center text-xs text-muted-foreground\">{emptyText}</li>\n            ) : (\n              filteredOptions.map((opt, idx) => {\n                const isSelected = opt.value === value;\n                const isHighlighted = idx === highlightedIndex;\n\n                return (\n                  <li\n                    key={String(opt.value)}\n                    className={cn(\n                      \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors\",\n                      isHighlighted && \"bg-accent text-accent-foreground\",\n                      isSelected && \"font-semibold\",\n                      opt.disabled && \"pointer-events-none opacity-50\"\n                    )}\n                    onClick={() => handleSelect(opt)}\n                    onMouseEnter={() => setHighlightedIndex(idx)}\n                  >\n                    {renderOption ? (\n                      renderOption(opt)\n                    ) : (\n                      <>\n                        {opt.icon && <span className=\"mr-2 shrink-0\">{opt.icon}</span>}\n                        <div className=\"flex flex-col flex-1\">\n                          <span>{opt.label}</span>\n                          {opt.description && (\n                            <span className=\"text-xs text-muted-foreground font-normal\">\n                              {opt.description}\n                            </span>\n                          )}\n                        </div>\n                        {opt.badge && (\n                          <span className=\"ml-2 font-mono text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground\">\n                            {opt.badge}\n                          </span>\n                        )}\n                        {isSelected && (\n                          <span className=\"ml-auto text-primary\">✓</span>\n                        )}\n                      </>\n                    )}\n                  </li>\n                );\n              })\n            )}\n          </ul>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}