{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "title": "Multi Select",
  "description": "A searchable multiple-value picker with limits, select-all and compact value summaries.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@rivelle/utils",
    "@rivelle/button",
    "@rivelle/command",
    "@rivelle/popover"
  ],
  "files": [
    {
      "path": "registry/nova/ui/multi-select.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Check, ChevronsUpDown, X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\n\ntype MultiSelectOption = {\n  value: string;\n  label: string;\n  disabled?: boolean;\n};\n\ntype MultiSelectProps = {\n  options: MultiSelectOption[];\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  emptyText?: string;\n  maxSelected?: number;\n  maxCount?: number;\n  selectAllText?: string;\n  clearText?: string;\n  closeOnSelect?: boolean;\n  disabled?: boolean;\n  className?: string;\n  contentClassName?: string;\n};\n\nfunction MultiSelect({\n  options,\n  value,\n  defaultValue = [],\n  onValueChange,\n  placeholder = \"Select options\",\n  searchPlaceholder = \"Search options...\",\n  emptyText = \"No results found.\",\n  maxSelected,\n  maxCount = 2,\n  selectAllText = \"Select all\",\n  clearText = \"Clear selection\",\n  closeOnSelect = false,\n  disabled,\n  className,\n  contentClassName,\n}: MultiSelectProps) {\n  const [open, setOpen] = React.useState(false);\n  const [internalValue, setInternalValue] = React.useState(defaultValue);\n  const selectedValues = value ?? internalValue;\n  const selectedSet = React.useMemo(\n    () => new Set(selectedValues),\n    [selectedValues],\n  );\n  const selectedOptions = options.filter((option) =>\n    selectedSet.has(option.value),\n  );\n  const enabledOptions = options.filter((option) => !option.disabled);\n  const allSelected =\n    enabledOptions.length > 0 &&\n    enabledOptions.every((option) => selectedSet.has(option.value));\n  const canSelectAll =\n    maxSelected === undefined || maxSelected >= enabledOptions.length;\n\n  const updateValue = (next: string[]) => {\n    if (value === undefined) setInternalValue(next);\n    onValueChange?.(next);\n  };\n\n  const toggle = (option: MultiSelectOption) => {\n    if (option.disabled) return;\n\n    if (selectedSet.has(option.value)) {\n      updateValue(selectedValues.filter((item) => item !== option.value));\n    } else if (\n      maxSelected === undefined ||\n      selectedValues.length < maxSelected\n    ) {\n      updateValue([...selectedValues, option.value]);\n    }\n\n    if (closeOnSelect) setOpen(false);\n  };\n\n  const toggleAll = () => {\n    if (allSelected) {\n      updateValue(\n        selectedValues.filter(\n          (value) => !enabledOptions.some((option) => option.value === value),\n        ),\n      );\n      return;\n    }\n\n    const existing = selectedValues.filter(\n      (value) => !enabledOptions.some((option) => option.value === value),\n    );\n    const available = enabledOptions.map((option) => option.value);\n    updateValue(\n      maxSelected === undefined\n        ? [...existing, ...available]\n        : [...existing, ...available].slice(0, maxSelected),\n    );\n  };\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          aria-expanded={open}\n          aria-haspopup=\"listbox\"\n          className={cn(\n            \"h-auto min-h-11.5 w-full justify-between gap-3 px-3.5 py-2 font-medium hover:-translate-y-0\",\n            !selectedOptions.length && \"text-muted-foreground\",\n            className,\n          )}\n          data-slot=\"multi-select\"\n          disabled={disabled}\n          role=\"combobox\"\n          variant=\"outline\"\n        >\n          <span className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1.5\">\n            {selectedOptions.length ? (\n              <>\n                {selectedOptions.slice(0, maxCount).map((option) => (\n                  <span\n                    className=\"inline-flex h-7 max-w-44 items-center rounded-lg border border-primary/12 bg-primary/9 px-2.5 text-xs font-semibold text-primary\"\n                    key={option.value}\n                  >\n                    <span className=\"truncate\">{option.label}</span>\n                  </span>\n                ))}\n                {selectedOptions.length > maxCount ? (\n                  <span className=\"inline-flex h-7 items-center rounded-lg border border-foreground/10 bg-muted/70 px-2.5 text-xs font-semibold text-muted-foreground\">\n                    +{selectedOptions.length - maxCount}\n                  </span>\n                ) : null}\n              </>\n            ) : (\n              <span className=\"truncate\">{placeholder}</span>\n            )}\n          </span>\n          <ChevronsUpDown className=\"size-4 shrink-0 text-muted-foreground\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className={cn(\n          \"w-[var(--radix-popover-trigger-width)] p-0\",\n          contentClassName,\n        )}\n      >\n        <Command>\n          <CommandInput placeholder={searchPlaceholder} />\n          <CommandList role=\"listbox\" aria-multiselectable=\"true\">\n            <CommandEmpty>{emptyText}</CommandEmpty>\n            <CommandGroup>\n              {canSelectAll ? (\n                <CommandItem onSelect={toggleAll} value={selectAllText}>\n                  <span\n                    className={cn(\n                      \"flex size-4 items-center justify-center rounded border border-foreground/20\",\n                      allSelected &&\n                        \"border-primary bg-primary text-primary-foreground\",\n                    )}\n                  >\n                    <Check\n                      className={cn(\"size-3\", !allSelected && \"opacity-0\")}\n                    />\n                  </span>\n                  {selectAllText}\n                </CommandItem>\n              ) : null}\n              {options.map((option) => {\n                const selected = selectedSet.has(option.value);\n                const atLimit =\n                  !selected &&\n                  maxSelected !== undefined &&\n                  selectedValues.length >= maxSelected;\n\n                return (\n                  <CommandItem\n                    aria-selected={selected}\n                    disabled={option.disabled || atLimit}\n                    key={option.value}\n                    onSelect={() => toggle(option)}\n                    value={`${option.label} ${option.value}`}\n                  >\n                    <span\n                      className={cn(\n                        \"flex size-4 items-center justify-center rounded border border-foreground/20\",\n                        selected &&\n                          \"border-primary bg-primary text-primary-foreground\",\n                      )}\n                    >\n                      <Check\n                        className={cn(\"size-3\", !selected && \"opacity-0\")}\n                      />\n                    </span>\n                    <span className=\"truncate\">{option.label}</span>\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n            {selectedValues.length ? (\n              <>\n                <CommandSeparator />\n                <CommandGroup>\n                  <CommandItem\n                    className=\"justify-center text-muted-foreground\"\n                    onSelect={() => updateValue([])}\n                    value={clearText}\n                  >\n                    <X className=\"size-3.5\" />\n                    {clearText}\n                  </CommandItem>\n                </CommandGroup>\n              </>\n            ) : null}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport { MultiSelect };\nexport type { MultiSelectOption, MultiSelectProps };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}