import React, { useState, useRef, useEffect } from 'react'; import { Check, ChevronDown, X } from 'lucide-react'; interface MultiSelectProps { options: { value: string; label: string }[]; selected: string[]; onChange: (selected: string[]) => void; placeholder?: string; disabled?: boolean; className?: string; } export const MultiSelect: React.FC = ({ options, selected, onChange, placeholder = 'Select items...', disabled = false, className = '', }) => { const [isOpen, setIsOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const dropdownRef = useRef(null); const inputRef = useRef(null); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); setSearchTerm(''); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()), ); const handleToggleOption = (value: string) => { if (disabled) return; const newSelected = selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]; onChange(newSelected); }; const handleRemoveItem = (value: string, e: React.MouseEvent) => { e.stopPropagation(); if (disabled) return; onChange(selected.filter((item) => item !== value)); }; const handleToggleDropdown = () => { if (disabled) return; setIsOpen(!isOpen); if (!isOpen) { setTimeout(() => inputRef.current?.focus(), 0); } }; const getSelectedLabels = () => { return selected .map((value) => options.find((opt) => opt.value === value)?.label || value) .filter(Boolean); }; return (
{/* Selected items display */}
{selected.length > 0 ? ( <> {getSelectedLabels().map((label, index) => ( {label} {!disabled && ( )} ))} ) : ( {placeholder} )}
{/* Dropdown menu */} {isOpen && !disabled && (
{/* Search input */}
setSearchTerm(e.target.value)} placeholder="Search..." className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" onClick={(e) => e.stopPropagation()} />
{/* Options list */}
{filteredOptions.length > 0 ? ( filteredOptions.map((option) => { const isSelected = selected.includes(option.value); return (
handleToggleOption(option.value)} className={` px-3 py-2 cursor-pointer flex items-center justify-between transition-colors duration-150 ${isSelected ? 'bg-blue-50 text-blue-700' : 'hover:bg-gray-100'} `} > {option.label} {isSelected && }
); }) ) : (
{searchTerm ? 'No results found' : 'No options available'}
)}
)}
); };