{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "multi-select", "description": "A multi-select input that emulates the Shadcn Select component as closely as possible.", "registryDependencies": ["popover", "command", "badge", "button"], "files": [ { "path": "src/registry/new-york/items/multi-select/components/multi-select.tsx", "content": "\"use client\"\n\nimport { CheckIcon, ChevronsUpDownIcon, XIcon } from \"lucide-react\"\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\"\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\"\nimport { Badge } from \"@/components/ui/badge\"\n\ntype MultiSelectContextType = {\n open: boolean\n setOpen: (open: boolean) => void\n selectedValues: Set\n toggleValue: (value: string) => void\n items: Map\n single: boolean\n onItemAdded: (value: string, label: ReactNode) => void\n}\nconst MultiSelectContext = createContext(null)\n\nexport function MultiSelect({\n children,\n values,\n defaultValues,\n onValuesChange,\n single = false,\n}: {\n children: ReactNode\n values?: string[]\n defaultValues?: string[]\n onValuesChange?: (values: string[]) => void\n single?: boolean\n}) {\n const [open, setOpen] = useState(false)\n const [internalValues, setInternalValues] = useState(\n new Set(values ?? defaultValues),\n )\n const selectedValues = values ? new Set(values) : internalValues\n const [items, setItems] = useState>(new Map())\n\n function toggleValue(value: string) {\n const getNewSet = (prev: Set) => {\n if (single) {\n return prev.has(value) ? new Set() : new Set([value])\n }\n const newSet = new Set(prev)\n if (newSet.has(value)) {\n newSet.delete(value)\n } else {\n newSet.add(value)\n }\n return newSet\n }\n setInternalValues(getNewSet)\n onValuesChange?.([...getNewSet(selectedValues)])\n if (single) setOpen(false)\n }\n\n const onItemAdded = useCallback((value: string, label: ReactNode) => {\n setItems(prev => {\n if (prev.get(value) === label) return prev\n return new Map(prev).set(value, label)\n })\n }, [])\n\n return (\n \n \n {children}\n \n \n )\n}\n\nexport function MultiSelectTrigger({\n className,\n children,\n ...props\n}: {\n className?: string\n children?: ReactNode\n} & ComponentPropsWithoutRef) {\n const { open } = useMultiSelectContext()\n\n return (\n \n \n {children}\n \n \n \n )\n}\n\nexport function MultiSelectValue({\n placeholder,\n clickToRemove = true,\n className,\n overflowBehavior = \"wrap-when-open\",\n ...props\n}: {\n placeholder?: string\n clickToRemove?: boolean\n overflowBehavior?: \"wrap\" | \"wrap-when-open\" | \"cutoff\"\n} & Omit, \"children\">) {\n const { selectedValues, toggleValue, items, open, single } =\n useMultiSelectContext()\n const [overflowAmount, setOverflowAmount] = useState(0)\n const valueRef = useRef(null)\n const overflowRef = useRef(null)\n\n const shouldWrap =\n overflowBehavior === \"wrap\" ||\n (overflowBehavior === \"wrap-when-open\" && open)\n\n const checkOverflow = useCallback(() => {\n if (valueRef.current == null) return\n\n const containerElement = valueRef.current\n const overflowElement = overflowRef.current\n const items = containerElement.querySelectorAll(\n \"[data-selected-item]\",\n )\n\n if (overflowElement != null) overflowElement.style.display = \"none\"\n items.forEach(child => child.style.removeProperty(\"display\"))\n let amount = 0\n for (let i = items.length - 1; i >= 0; i--) {\n const child = items[i]!\n if (containerElement.scrollWidth <= containerElement.clientWidth) {\n break\n }\n amount = items.length - i\n child.style.display = \"none\"\n overflowElement?.style.removeProperty(\"display\")\n }\n setOverflowAmount(amount)\n }, [])\n\n const handleResize = useCallback(\n (node: HTMLDivElement) => {\n valueRef.current = node\n\n const mutationObserver = new MutationObserver(checkOverflow)\n const observer = new ResizeObserver(debounce(checkOverflow, 100))\n\n mutationObserver.observe(node, {\n childList: true,\n attributes: true,\n attributeFilter: [\"class\", \"style\"],\n })\n observer.observe(node)\n\n return () => {\n observer.disconnect()\n mutationObserver.disconnect()\n valueRef.current = null\n }\n },\n [checkOverflow],\n )\n\n if (selectedValues.size === 0 && placeholder) {\n return (\n \n {placeholder}\n \n )\n }\n\n if (single && selectedValues.size > 0) {\n return (\n \n {items.get([...selectedValues][0])}\n \n )\n }\n\n return (\n \n {[...selectedValues]\n .filter(value => items.has(value))\n .map(value => (\n {\n e.stopPropagation()\n toggleValue(value)\n }\n : undefined\n }\n >\n {items.get(value)}\n {clickToRemove && (\n \n )}\n \n ))}\n 0 && !shouldWrap ? \"block\" : \"none\",\n }}\n variant=\"outline\"\n ref={overflowRef}\n >\n +{overflowAmount}\n \n \n )\n}\n\nexport function MultiSelectContent({\n search = true,\n children,\n ...props\n}: {\n search?: boolean | { placeholder?: string; emptyMessage?: string }\n children: ReactNode\n} & Omit, \"children\">) {\n const canSearch = typeof search === \"object\" ? true : search\n\n return (\n <>\n
\n \n {children}\n \n
\n \n \n {canSearch ? (\n \n ) : (\n