import React, { useState, useMemo, useRef, useCallback, type ReactElement, type ReactNode } from 'react'; import { Popover, PopoverTrigger, PopoverContent } from './Popover'; interface SearchableSelectProps { items: T[]; selectedId?: string; onSelect: (item: T) => void; filterFn: (item: T, query: string) => boolean; renderItem: (item: T, state: { isSelected: boolean; isFocused: boolean }) => ReactNode; renderTrigger: (state: { open: boolean }) => ReactElement; headerContent?: ReactNode; placeholder?: string; emptyMessage?: string; align?: 'start' | 'center' | 'end'; width?: string; onOpenChange?: (open: boolean) => void; } export function SearchableSelect({ items, selectedId, onSelect, filterFn, renderItem, renderTrigger, headerContent, placeholder = 'Search...', emptyMessage = 'No results', align = 'start', width = 'w-64', onOpenChange, }: SearchableSelectProps) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(''); const [focusedIndex, setFocusedIndex] = useState(-1); const listRef = useRef(null); const inputRef = useRef(null); const filtered = useMemo( () => (search ? items.filter((item) => filterFn(item, search)) : items), [items, search, filterFn], ); const handleOpenChange = useCallback( (next: boolean) => { setOpen(next); if (!next) { setSearch(''); setFocusedIndex(-1); } onOpenChange?.(next); }, [onOpenChange], ); const scrollToIndex = useCallback((i: number) => { listRef.current?.querySelector(`[data-index="${i}"]`)?.scrollIntoView({ block: 'nearest' }); }, []); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { const len = filtered.length; if (len === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); const next = focusedIndex < len - 1 ? focusedIndex + 1 : 0; setFocusedIndex(next); scrollToIndex(next); } else if (e.key === 'ArrowUp') { e.preventDefault(); const next = focusedIndex > 0 ? focusedIndex - 1 : len - 1; setFocusedIndex(next); scrollToIndex(next); } else if (e.key === 'Enter') { e.preventDefault(); if (focusedIndex >= 0 && filtered[focusedIndex]) { onSelect(filtered[focusedIndex]); handleOpenChange(false); } } }, [filtered, focusedIndex, onSelect, handleOpenChange, scrollToIndex], ); return ( {/* Search row */}
{ setSearch(e.target.value); setFocusedIndex(-1); }} onKeyDown={handleKeyDown} placeholder={placeholder} className="w-full bg-transparent text-xs text-foreground placeholder:text-muted-foreground/50 outline-none" />
{headerContent} {/* Item list */}
{filtered.length === 0 ? (

{emptyMessage}

) : ( filtered.map((item, i) => ( )) )}
); }