{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "type": "registry:component",
  "title": "Multi Select",
  "description": "Popover-based multi-selection input with selected-value badges and optional search.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "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 = React.forwardRef<\n  HTMLButtonElement,\n  MultiSelectTriggerProps\n>(\n  (\n    {\n      className,\n      contentId,\n      disabled = false,\n      onKeyDown,\n      open,\n      placeholder = \"Select options\",\n      selectedOptions,\n      ...props\n    },\n    ref,\n  ) => (\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  ),\n);\nMultiSelectTrigger.displayName = \"MultiSelectTrigger\";\n\nconst MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(\n  (\n    {\n      defaultValue = [],\n      emptyText = \"No options found.\",\n      onKeyDown,\n      onOpenChange,\n      onValueChange,\n      options,\n      searchable = false,\n      searchPlaceholder = \"Search options...\",\n      value,\n      ...props\n    },\n    ref,\n  ) => {\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  },\n);\nMultiSelect.displayName = \"MultiSelect\";\n\nexport { MultiSelect };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
