{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"combobox","type":"registry:ui","title":"Combobox","description":"A searchable select — type to filter a long list of options.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/button.json","https://dashral.com/r/command.json","https://dashral.com/r/field.json","https://dashral.com/r/popover.json"],"files":[{"path":"registry/dashral/combobox.tsx","type":"registry:file","target":"components/dashral/combobox.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { CheckIcon, ChevronsUpDownIcon } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { Button } from \"./button\"\nimport { FieldWrapper, splitFieldProps, useFieldControl, type FieldClassNames, type FieldControlProps, type FieldOwnProps } from \"./field\"\nimport { CommandRoot, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from \"./command\"\nimport { PopoverRoot, PopoverContent, PopoverTrigger } from \"./popover\"\n\nexport type ComboboxOption = {\n  value: string\n  label: string\n  /** Before the label, in the list and in the field once picked: an icon, an avatar, a flag. */\n  icon?: React.ReactNode\n  /** A second line under the label, e.g. an email or a role. It's searched too. */\n  description?: string\n  disabled?: boolean\n}\n\ntype ComboboxProps = FieldOwnProps & {\n  options: ComboboxOption[]\n  /** Controlled value. Omit it (and use defaultValue) for an uncontrolled combobox. */\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  placeholder?: string\n  searchPlaceholder?: string\n  emptyText?: string\n  id?: string\n  disabled?: boolean\n  required?: boolean\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"]\n  /** The trigger's classes. */\n  className?: string\n  \"aria-label\"?: string\n  classNames?: FieldClassNames & { trigger?: string; content?: string; item?: string }\n}\n\n/**\n * A searchable select built from Popover and Command. Selecting the current value again clears it. Give it a label,\n * description or error and it becomes a whole field.\n */\nfunction Combobox(props: ComboboxProps) {\n  const [field, { required: _required, ...control }] = splitFieldProps(props)\n  return (\n    <FieldWrapper {...field}>\n      <ComboboxControl {...control} />\n    </FieldWrapper>\n  )\n}\n\nfunction ComboboxControl({\n  options,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  placeholder: placeholderProp,\n  searchPlaceholder: searchPlaceholderProp,\n  emptyText: emptyTextProp,\n  className,\n  classNames,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: Omit<ComboboxProps, keyof FieldOwnProps | \"required\">) {\n  const { t } = useLocale()\n  const emptyText = emptyTextProp ?? t(\"noResults\")\n  const searchPlaceholder = searchPlaceholderProp ?? t(\"search\")\n  const placeholder = placeholderProp ?? t(\"selectOption\")\n  const { id, disabled, ...aria } = useFieldControl<FieldControlProps>({ id: rest.id, disabled: rest.disabled, \"aria-invalid\": rest[\"aria-invalid\"] })\n  const [open, setOpen] = React.useState(false)\n  const [internal, setInternal] = React.useState(defaultValue)\n  const current = value ?? internal\n  const selected = options.find((option) => option.value === current)\n\n  function select(next: string) {\n    const newValue = next === current ? \"\" : next\n    if (value === undefined) setInternal(newValue)\n    onValueChange?.(newValue)\n    setOpen(false)\n  }\n\n  return (\n    <PopoverRoot open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          id={id}\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-label={ariaLabel}\n          aria-describedby={aria[\"aria-describedby\"]}\n          aria-invalid={aria[\"aria-invalid\"]}\n          disabled={disabled}\n          className={cn(\n            \"w-full min-w-0 justify-between font-normal aria-invalid:border-destructive\",\n            // Looks like the other fields (Input, Select), not like a button.\n            \"border-input bg-transparent hover:bg-transparent focus-visible:border-ring focus-visible:ring-ring/15 motion-safe:enabled:not-aria-busy:active:scale-100\",\n            selected ? \"hover:text-foreground\" : \"text-muted-foreground hover:text-muted-foreground\",\n            className,\n            classNames?.trigger\n          )}\n        >\n          <span className=\"flex min-w-0 items-center gap-2\">\n            {selected?.icon}\n            <span className=\"truncate\">{selected?.label ?? placeholder}</span>\n          </span>\n          <ChevronsUpDownIcon className=\"opacity-50\" aria-hidden />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className={cn(\"w-(--radix-popover-trigger-width) min-w-48 p-0\", classNames?.content)} align=\"start\">\n        <CommandRoot>\n          <CommandInput placeholder={searchPlaceholder} />\n          <CommandList>\n            <CommandEmpty>{emptyText}</CommandEmpty>\n            <CommandGroup>\n              {options.map((option) => (\n                <CommandItem\n                  key={option.value}\n                  value={option.label}\n                  keywords={option.description ? [option.description] : undefined}\n                  disabled={option.disabled}\n                  onSelect={() => select(option.value)}\n                  className={classNames?.item}\n                >\n                  <CheckIcon className={cn(option.value === current ? \"opacity-100\" : \"opacity-0\")} aria-hidden />\n                  {option.icon}\n                  {option.description ? (\n                    <span className=\"grid min-w-0\">\n                      <span className=\"truncate\">{option.label}</span>\n                      <span className=\"truncate text-xs text-muted-foreground\">{option.description}</span>\n                    </span>\n                  ) : (\n                    option.label\n                  )}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </CommandRoot>\n      </PopoverContent>\n    </PopoverRoot>\n  )\n}\n\nexport { Combobox }\n"}]}