"use client" import { useState, useRef, useEffect } from "react" import { createPortal } from "react-dom" import { cn } from "@/lib/utils" interface Option { value: string label: string count?: number } interface MultiSelectProps { label: string options: Option[] selected: string[] onChange: (selected: string[]) => void searchable?: boolean placeholder?: string } export function MultiSelect({ label, options, selected, onChange, searchable = true, placeholder, }: MultiSelectProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState("") const [position, setPosition] = useState({ top: 0, left: 0, width: 0 }) const triggerRef = useRef(null) const dropdownRef = useRef(null) const inputRef = useRef(null) // Calculate dropdown position useEffect(() => { if (open && triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect() setPosition({ top: rect.bottom + 4, left: rect.left, width: Math.max(rect.width, 240), }) } }, [open]) // Focus search input when opened useEffect(() => { if (open && searchable && inputRef.current) { setTimeout(() => inputRef.current?.focus(), 0) } }, [open, searchable]) // Click outside handler useEffect(() => { function handleClickOutside(event: MouseEvent) { const target = event.target as Node if ( triggerRef.current && !triggerRef.current.contains(target) && dropdownRef.current && !dropdownRef.current.contains(target) ) { setOpen(false) setSearch("") } } if (open) { document.addEventListener("mousedown", handleClickOutside) } return () => document.removeEventListener("mousedown", handleClickOutside) }, [open]) // Filter options based on search const filteredOptions = search ? options.filter((opt) => opt.label.toLowerCase().includes(search.toLowerCase())) : options const toggleOption = (value: string) => { if (selected.includes(value)) { onChange(selected.filter((v) => v !== value)) } else { onChange([...selected, value]) } } const displayText = selected.length === 0 ? placeholder || label : selected.length === 1 ? options.find((o) => o.value === selected[0])?.label || selected[0] : selected.length <= 2 ? selected.map((v) => options.find((o) => o.value === v)?.label || v).join(", ") : `${selected.length} selected` return ( <> {/* Trigger button */} {/* Dropdown - rendered via portal */} {open && typeof document !== "undefined" && createPortal(
{/* Search input */} {searchable && (
setSearch(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm bg-[#222222] border border-[#333333] rounded text-text-primary placeholder-text-muted focus:outline-none focus:border-accent" />
)} {/* Options list */}
{filteredOptions.length === 0 ? (
No options found
) : ( filteredOptions.map((option) => { const isSelected = selected.includes(option.value) return ( ) }) )}
, document.body )} ) }