{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "title": "Multi-select",
  "description": "Multi-select dropdown",
  "registryDependencies": [
    "popover",
    "command",
    "badge",
    "button"
  ],
  "files": [
    {
      "path": "src/components/ui/multi-select.tsx",
      "content": "'use client'\n\nimport { CheckIcon, ChevronsUpDownIcon, XIcon } from 'lucide-react'\nimport {\n\ttype ComponentPropsWithoutRef,\n\tcreateContext,\n\ttype ReactNode,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport {\n\tCommand,\n\tCommandEmpty,\n\tCommandGroup,\n\tCommandInput,\n\tCommandItem,\n\tCommandList,\n\tCommandSeparator,\n} from '@/components/ui/command'\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from '@/components/ui/popover'\nimport { cn } from '@/lib/utils'\n\ntype MultiSelectContextType = {\n\topen: boolean\n\tsetOpen: (open: boolean) => void\n\tselectedValues: Set<string>\n\ttoggleValue: (value: string) => void\n\titems: Map<string, ReactNode>\n\tonItemAdded: (value: string, label: ReactNode) => void\n}\nconst MultiSelectContext = createContext<MultiSelectContextType | null>(null)\n\nexport function MultiSelect({\n\tchildren,\n\tvalues,\n\tdefaultValues,\n\tonValuesChange,\n}: {\n\tchildren: ReactNode\n\tvalues?: string[]\n\tdefaultValues?: string[]\n\tonValuesChange?: (values: string[]) => void\n}) {\n\tconst [open, setOpen] = useState(false)\n\tconst [internalValues, setInternalValues] = useState(\n\t\tnew Set<string>(values ?? defaultValues),\n\t)\n\tconst selectedValues = values ? new Set(values) : internalValues\n\tconst [items, setItems] = useState<Map<string, ReactNode>>(new Map())\n\n\tfunction toggleValue(value: string) {\n\t\tconst getNewSet = (prev: Set<string>) => {\n\t\t\tconst newSet = new Set(prev)\n\t\t\tif (newSet.has(value)) {\n\t\t\t\tnewSet.delete(value)\n\t\t\t} else {\n\t\t\t\tnewSet.add(value)\n\t\t\t}\n\t\t\treturn newSet\n\t\t}\n\t\tsetInternalValues(getNewSet)\n\t\tonValuesChange?.([...getNewSet(selectedValues)])\n\t}\n\n\tconst onItemAdded = useCallback((value: string, label: ReactNode) => {\n\t\tsetItems((prev) => {\n\t\t\tif (prev.get(value) === label) return prev\n\t\t\treturn new Map(prev).set(value, label)\n\t\t})\n\t}, [])\n\n\treturn (\n\t\t<MultiSelectContext\n\t\t\tvalue={{\n\t\t\t\topen,\n\t\t\t\tsetOpen,\n\t\t\t\tselectedValues,\n\t\t\t\ttoggleValue,\n\t\t\t\titems,\n\t\t\t\tonItemAdded,\n\t\t\t}}\n\t\t>\n\t\t\t<Popover open={open} onOpenChange={setOpen} modal={true}>\n\t\t\t\t{children}\n\t\t\t</Popover>\n\t\t</MultiSelectContext>\n\t)\n}\n\nexport function MultiSelectTrigger({\n\tclassName,\n\tchildren,\n\t...props\n}: {\n\tclassName?: string\n\tchildren?: ReactNode\n} & ComponentPropsWithoutRef<typeof Button>) {\n\tconst { open } = useMultiSelectContext()\n\n\treturn (\n\t\t<PopoverTrigger asChild>\n\t\t\t<Button\n\t\t\t\t{...props}\n\t\t\t\tvariant={props.variant ?? 'outline'}\n\t\t\t\trole={props.role ?? 'combobox'}\n\t\t\t\taria-expanded={props['aria-expanded'] ?? open}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex h-auto min-h-9 w-fit items-center justify-between gap-2 overflow-hidden rounded-md border border-input bg-transparent px-3 py-1.5 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t<ChevronsUpDownIcon className=\"size-4 shrink-0 opacity-50\" />\n\t\t\t</Button>\n\t\t</PopoverTrigger>\n\t)\n}\n\nexport function MultiSelectValue({\n\tplaceholder,\n\tclickToRemove = true,\n\tclassName,\n\toverflowBehavior = 'wrap',\n\t...props\n}: {\n\tplaceholder?: string\n\tclickToRemove?: boolean\n\toverflowBehavior?: 'wrap' | 'wrap-when-open' | 'cutoff'\n} & Omit<ComponentPropsWithoutRef<'div'>, 'children'>) {\n\tconst { selectedValues, toggleValue, items, open } = useMultiSelectContext()\n\tconst [overflowAmount, setOverflowAmount] = useState(0)\n\tconst valueRef = useRef<HTMLDivElement>(null)\n\tconst overflowRef = useRef<HTMLDivElement>(null)\n\n\tconst shouldWrap =\n\t\toverflowBehavior === 'wrap' ||\n\t\t(overflowBehavior === 'wrap-when-open' && open)\n\n\tconst checkOverflow = useCallback(() => {\n\t\tif (valueRef.current == null) return\n\n\t\tconst containerElement = valueRef.current\n\t\tconst overflowElement = overflowRef.current\n\t\tconst items = containerElement.querySelectorAll<HTMLElement>(\n\t\t\t'[data-selected-item]',\n\t\t)\n\n\t\tif (overflowElement != null) overflowElement.style.display = 'none'\n\t\titems.forEach((child) => child.style.removeProperty('display'))\n\t\tlet amount = 0\n\t\tfor (let i = items.length - 1; i >= 0; i--) {\n\t\t\tconst child = items[i]!\n\t\t\tif (containerElement.scrollWidth <= containerElement.clientWidth) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tamount = items.length - i\n\t\t\tchild.style.display = 'none'\n\t\t\toverflowElement?.style.removeProperty('display')\n\t\t}\n\t\tsetOverflowAmount(amount)\n\t}, [])\n\n\tconst handleResize = useCallback(\n\t\t(node: HTMLDivElement) => {\n\t\t\tvalueRef.current = node\n\n\t\t\tconst observer = new ResizeObserver(checkOverflow)\n\t\t\tobserver.observe(node)\n\n\t\t\treturn () => {\n\t\t\t\tobserver.disconnect()\n\t\t\t\tvalueRef.current = null\n\t\t\t}\n\t\t},\n\t\t[checkOverflow],\n\t)\n\n\tif (selectedValues.size === 0 && placeholder) {\n\t\treturn (\n\t\t\t<span className=\"min-w-0 overflow-hidden font-normal text-muted-foreground\">\n\t\t\t\t{placeholder}\n\t\t\t</span>\n\t\t)\n\t}\n\n\treturn (\n\t\t// @ts-expect-error caused when upgrading to Nextjs 16\n\t\t<div\n\t\t\t{...props}\n\t\t\tref={handleResize}\n\t\t\tclassName={cn(\n\t\t\t\t'flex w-fit gap-1.5 overflow-hidden',\n\t\t\t\tshouldWrap && 'h-full flex-wrap',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t{[...selectedValues]\n\t\t\t\t.filter((value) => items.has(value))\n\t\t\t\t.map((value) => (\n\t\t\t\t\t<Badge\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tdata-selected-item\n\t\t\t\t\t\tclassName=\"group flex items-center gap-1\"\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\tonClick={\n\t\t\t\t\t\t\tclickToRemove\n\t\t\t\t\t\t\t\t? (e) => {\n\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\ttoggleValue(value)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{items.get(value)}\n\t\t\t\t\t\t{clickToRemove && (\n\t\t\t\t\t\t\t<XIcon className=\"size-2 text-muted-foreground group-hover:text-destructive\" />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Badge>\n\t\t\t\t))}\n\t\t\t<Badge\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: overflowAmount > 0 && !shouldWrap ? 'block' : 'none',\n\t\t\t\t}}\n\t\t\t\tvariant=\"outline\"\n\t\t\t\tref={overflowRef}\n\t\t\t>\n\t\t\t\t+{overflowAmount}\n\t\t\t</Badge>\n\t\t</div>\n\t)\n}\n\nexport function MultiSelectContent({\n\tsearch = true,\n\tchildren,\n\t...props\n}: {\n\tsearch?: boolean | { placeholder?: string; emptyMessage?: string }\n\tchildren: ReactNode\n} & Omit<ComponentPropsWithoutRef<typeof Command>, 'children'>) {\n\tconst canSearch = typeof search === 'object' ? true : search\n\n\treturn (\n\t\t<>\n\t\t\t<div style={{ display: 'none' }}>\n\t\t\t\t<Command>\n\t\t\t\t\t<CommandList>{children}</CommandList>\n\t\t\t\t</Command>\n\t\t\t</div>\n\t\t\t<PopoverContent className=\"min-w-[var(--radix-popover-trigger-width)] p-0\">\n\t\t\t\t<Command {...props}>\n\t\t\t\t\t{canSearch ? (\n\t\t\t\t\t\t<CommandInput\n\t\t\t\t\t\t\tplaceholder={\n\t\t\t\t\t\t\t\ttypeof search === 'object' ? search.placeholder : undefined\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<button autoFocus className=\"sr-only\" />\n\t\t\t\t\t)}\n\t\t\t\t\t<CommandList>\n\t\t\t\t\t\t{canSearch && (\n\t\t\t\t\t\t\t<CommandEmpty>\n\t\t\t\t\t\t\t\t{typeof search === 'object' ? search.emptyMessage : undefined}\n\t\t\t\t\t\t\t</CommandEmpty>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</CommandList>\n\t\t\t\t</Command>\n\t\t\t</PopoverContent>\n\t\t</>\n\t)\n}\n\nexport function MultiSelectItem({\n\tvalue,\n\tchildren,\n\tbadgeLabel,\n\tonSelect,\n\t...props\n}: {\n\tbadgeLabel?: ReactNode\n\tvalue: string\n} & Omit<ComponentPropsWithoutRef<typeof CommandItem>, 'value'>) {\n\tconst { toggleValue, selectedValues, onItemAdded } = useMultiSelectContext()\n\tconst isSelected = selectedValues.has(value)\n\n\tuseEffect(() => {\n\t\tonItemAdded(value, badgeLabel ?? children)\n\t}, [value, children, onItemAdded, badgeLabel])\n\n\treturn (\n\t\t<CommandItem\n\t\t\t{...props}\n\t\t\tonSelect={() => {\n\t\t\t\ttoggleValue(value)\n\t\t\t\tonSelect?.(value)\n\t\t\t}}\n\t\t>\n\t\t\t<CheckIcon\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'mr-2 ml-1.5 size-4',\n\t\t\t\t\tisSelected ? 'opacity-100' : 'opacity-0',\n\t\t\t\t)}\n\t\t\t/>\n\t\t\t{children}\n\t\t</CommandItem>\n\t)\n}\n\nexport function MultiSelectGroup(\n\tprops: ComponentPropsWithoutRef<typeof CommandGroup>,\n) {\n\treturn <CommandGroup {...props} />\n}\n\nexport function MultiSelectSeparator(\n\tprops: ComponentPropsWithoutRef<typeof CommandSeparator>,\n) {\n\treturn <CommandSeparator {...props} />\n}\n\nfunction useMultiSelectContext() {\n\tconst context = useContext(MultiSelectContext)\n\tif (context == null) {\n\t\tthrow new Error(\n\t\t\t'useMultiSelectContext must be used within a MultiSelectContext',\n\t\t)\n\t}\n\treturn context\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}