{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "title": "Multi Select",
  "description": "Popover-based multi-selection input with selected-value badges and optional search.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/multi-select/multi-select.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Check, ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } 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 MultiSelectOption = {\n  disabled?: boolean;\n  label: string;\n  value: string;\n};\n\nexport type MultiSelectProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  \"defaultValue\" | \"onChange\" | \"value\"\n> & {\n  defaultValue?: string[];\n  emptyText?: string;\n  onOpenChange?: (open: boolean) => void;\n  onValueChange?: (value: string[]) => void;\n  options: MultiSelectOption[];\n  placeholder?: string;\n  searchable?: boolean;\n  searchPlaceholder?: string;\n  value?: string[];\n};\n\ntype TriggerContentProps = {\n  placeholder: string;\n  selectedOptions: MultiSelectOption[];\n};\n\ntype OptionListProps = {\n  disabled: boolean;\n  emptyText: string;\n  onSelect: (value: string) => void;\n  options: MultiSelectOption[];\n  searchable: boolean;\n  searchPlaceholder: string;\n  selectedValues: string[];\n};\n\ntype MultiSelectStateOptions = {\n  defaultValue: string[];\n  onOpenChange?: (open: boolean) => void;\n  onValueChange?: (value: string[]) => void;\n  value?: string[];\n};\n\ntype MultiSelectTriggerProps = Omit<\n  MultiSelectProps,\n  | \"defaultValue\"\n  | \"emptyText\"\n  | \"onOpenChange\"\n  | \"onValueChange\"\n  | \"options\"\n  | \"searchable\"\n  | \"searchPlaceholder\"\n  | \"value\"\n> & {\n  contentId: string;\n  open: boolean;\n  selectedOptions: MultiSelectOption[];\n};\n\ntype MultiSelectContentProps = {\n  contentId: string;\n  disabled: boolean;\n  emptyText: string;\n  onSelect: (value: string) => void;\n  options: MultiSelectOption[];\n  searchable: boolean;\n  searchPlaceholder: string;\n  selectedValues: string[];\n};\n\nfunction getUniqueValues(values: string[]) {\n  return values.filter((value, index) => values.indexOf(value) === index);\n}\n\nfunction shouldOpenFromKey(key: string) {\n  return key === \" \" || key === \"ArrowDown\" || key === \"Enter\";\n}\n\nfunction TriggerContent({ placeholder, selectedOptions }: TriggerContentProps) {\n  if (selectedOptions.length === 0) {\n    return <span>{placeholder}</span>;\n  }\n\n  return (\n    <>\n      {selectedOptions.map((option) => (\n        <Badge className=\"max-w-full\" key={option.value} variant=\"secondary\">\n          <span className=\"truncate\">{option.label}</span>\n        </Badge>\n      ))}\n    </>\n  );\n}\n\nfunction OptionList({\n  disabled,\n  emptyText,\n  onSelect,\n  options,\n  searchable,\n  searchPlaceholder,\n  selectedValues,\n}: OptionListProps) {\n  return (\n    <Command>\n      {searchable ? <CommandInput placeholder={searchPlaceholder} /> : null}\n      <CommandList aria-multiselectable=\"true\">\n        <CommandEmpty>{emptyText}</CommandEmpty>\n        <CommandGroup>\n          <div>\n            {options.map((option) => {\n              const isSelected = selectedValues.includes(option.value);\n\n              return (\n                <CommandItem\n                  aria-disabled={option.disabled || undefined}\n                  aria-selected={isSelected}\n                  className=\"gap-2\"\n                  disabled={disabled || option.disabled}\n                  key={option.value}\n                  onSelect={() => {\n                    onSelect(option.value);\n                  }}\n                  role=\"option\"\n                  value={option.label}\n                >\n                  <span\n                    className={cn(\n                      \"flex size-4 items-center justify-center rounded-sm border border-input bg-background text-primary transition-opacity\",\n                      isSelected ? \"opacity-100\" : \"opacity-50\",\n                    )}\n                  >\n                    {isSelected ? <Check className=\"size-3.5\" /> : null}\n                  </span>\n                  <span className=\"flex-1\">{option.label}</span>\n                </CommandItem>\n              );\n            })}\n          </div>\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  );\n}\n\nfunction MultiSelectContent({\n  contentId,\n  disabled,\n  emptyText,\n  onSelect,\n  options,\n  searchable,\n  searchPlaceholder,\n  selectedValues,\n}: MultiSelectContentProps) {\n  return (\n    <PopoverContent\n      align=\"start\"\n      className=\"w-[var(--radix-popover-trigger-width)] p-0\"\n      id={contentId}\n    >\n      <OptionList\n        disabled={disabled}\n        emptyText={emptyText}\n        onSelect={onSelect}\n        options={options}\n        searchable={searchable}\n        searchPlaceholder={searchPlaceholder}\n        selectedValues={selectedValues}\n      />\n    </PopoverContent>\n  );\n}\n\nfunction useMultiSelectState({\n  defaultValue,\n  onOpenChange,\n  onValueChange,\n  value,\n}: MultiSelectStateOptions) {\n  const [open, setOpen] = React.useState(false);\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(() =>\n    getUniqueValues(defaultValue),\n  );\n  const isControlled = value !== undefined;\n  const selectedValues = React.useMemo(\n    () => getUniqueValues(value ?? uncontrolledValue),\n    [uncontrolledValue, value],\n  );\n\n  const setSelectedValues = React.useCallback(\n    (nextValue: string[]) => {\n      const uniqueValues = getUniqueValues(nextValue);\n\n      if (!isControlled) {\n        setUncontrolledValue(uniqueValues);\n      }\n\n      onValueChange?.(uniqueValues);\n    },\n    [isControlled, onValueChange],\n  );\n\n  const handleOpenChange = React.useCallback(\n    (nextOpen: boolean) => {\n      setOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n    },\n    [onOpenChange],\n  );\n\n  return {\n    handleOpenChange,\n    open,\n    selectedValues,\n    setSelectedValues,\n  };\n}\n\nconst MultiSelectTrigger = ({\n  className,\n  contentId,\n  disabled = false,\n  onKeyDown,\n  open,\n  placeholder = \"Select options\",\n  ref,\n  selectedOptions,\n  ...props\n}: MultiSelectTriggerProps & { ref?: React.Ref<HTMLButtonElement> }) => (\n  <Button\n    aria-controls={contentId}\n    aria-expanded={open}\n    aria-haspopup=\"listbox\"\n    className={cn(\n      \"min-h-10 w-full justify-between px-3 py-2 text-sm font-normal\",\n      selectedOptions.length === 0 && \"text-muted-foreground\",\n      className,\n    )}\n    disabled={disabled}\n    onKeyDown={onKeyDown}\n    ref={ref}\n    role=\"combobox\"\n    type=\"button\"\n    variant=\"outline\"\n    {...props}\n  >\n    <span className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1 text-left\">\n      <TriggerContent\n        placeholder={placeholder}\n        selectedOptions={selectedOptions}\n      />\n    </span>\n    <ChevronDown className=\"ml-2 size-4 shrink-0 opacity-50\" />\n  </Button>\n);\nMultiSelectTrigger.displayName = \"MultiSelectTrigger\";\n\nconst MultiSelect = ({\n  defaultValue = [],\n  emptyText = \"No options found.\",\n  onKeyDown,\n  onOpenChange,\n  onValueChange,\n  options,\n  ref,\n  searchable = false,\n  searchPlaceholder = \"Search options...\",\n  value,\n  ...props\n}: MultiSelectProps & { ref?: React.Ref<HTMLButtonElement> }) => {\n  const contentId = React.useId();\n  const { handleOpenChange, open, selectedValues, setSelectedValues } =\n    useMultiSelectState({ defaultValue, onOpenChange, onValueChange, value });\n  const selectedOptions = React.useMemo(\n    () => options.filter((option) => selectedValues.includes(option.value)),\n    [options, selectedValues],\n  );\n\n  const handleSelect = React.useCallback(\n    (nextValue: string) => {\n      const nextSelectedValues = selectedValues.includes(nextValue)\n        ? selectedValues.filter((valueItem) => valueItem !== nextValue)\n        : [...selectedValues, nextValue];\n\n      setSelectedValues(nextSelectedValues);\n    },\n    [selectedValues, setSelectedValues],\n  );\n\n  const handleTriggerKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      onKeyDown?.(event);\n\n      if (event.defaultPrevented || props.disabled) {\n        return;\n      }\n\n      if (shouldOpenFromKey(event.key)) {\n        event.preventDefault();\n        handleOpenChange(true);\n      }\n    },\n    [handleOpenChange, onKeyDown, props.disabled],\n  );\n\n  return (\n    <Popover onOpenChange={handleOpenChange} open={open}>\n      <PopoverTrigger asChild>\n        <MultiSelectTrigger\n          {...props}\n          contentId={contentId}\n          onKeyDown={handleTriggerKeyDown}\n          open={open}\n          ref={ref}\n          selectedOptions={selectedOptions}\n        />\n      </PopoverTrigger>\n      <MultiSelectContent\n        contentId={contentId}\n        disabled={props.disabled || false}\n        emptyText={emptyText}\n        onSelect={handleSelect}\n        options={options}\n        searchable={searchable}\n        searchPlaceholder={searchPlaceholder}\n        selectedValues={selectedValues}\n      />\n    </Popover>\n  );\n};\nMultiSelect.displayName = \"MultiSelect\";\n\nexport { MultiSelect };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
