"use client" import type React from "react" import { useState, useRef, useEffect, forwardRef } from "react" import { Search, X } from "lucide-react" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" export interface IEntry { value: string label: string } interface SearchInputProps { value?: IEntry onChange?: (value: IEntry) => void onSelect?: (value: IEntry) => void name?: string placeholder?: string entries?: IEntry[] disabled?: boolean className?: string } export const SearchInput = forwardRef( ( { value: controlledValue, onChange, onSelect, name, placeholder = "Search entries...", entries = [], disabled = false, className, ...props }, ref, ) => { const [internalValue, setInternalValue] = useState(null) const [isOpen, setIsOpen] = useState(false) const [filteredEntries, setFilteredEntries] = useState([]) const [selectedIndex, setSelectedIndex] = useState(-1) const internalRef = useRef(null) const listRef = useRef(null) const query = controlledValue?.label ?? internalValue?.label ?? "" const inputRef = (ref as React.RefObject) || internalRef // Filter entries based on query useEffect(() => { if (query.trim()) { const filtered = entries.filter((entry) => entry.label.toLowerCase().includes(query.toLowerCase()), ) setFilteredEntries(filtered) setSelectedIndex(-1) } else { setFilteredEntries([]) } }, [query, entries]) const handleValueChange = (newValue: IEntry | null) => { if (controlledValue === undefined) { setInternalValue(newValue) } if (newValue) { onChange?.(newValue) } } // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen || filteredEntries.length === 0) return switch (e.key) { case "ArrowDown": e.preventDefault() setSelectedIndex((prev) => prev < filteredEntries.length - 1 ? prev + 1 : 0, ) break case "ArrowUp": e.preventDefault() setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredEntries.length - 1, ) break case "Enter": e.preventDefault() if (selectedIndex >= 0) { handleSelect(filteredEntries[selectedIndex]) } break case "Escape": setIsOpen(false) setSelectedIndex(-1) inputRef.current?.blur() break } } const handleSelect = (entry: IEntry) => { handleValueChange(entry) onSelect?.(entry) setIsOpen(false) setSelectedIndex(-1) inputRef.current?.blur() } const clearSearch = () => { handleValueChange(null) setIsOpen(false) setSelectedIndex(-1) inputRef.current?.focus() } return (
{ const newLabel = e.target.value handleValueChange({ value: newLabel, label: newLabel }) }} onFocus={() => !disabled && setIsOpen(true)} onBlur={() => { // Delay closing to allow clicking on entries setTimeout(() => setIsOpen(false), 150) }} onKeyDown={handleKeyDown} className="pl-10 pr-10" /> {query && !disabled && ( )}
{/* Results dropdown */} {isOpen && !disabled && filteredEntries.length > 0 && (
    {filteredEntries.map((entry, index) => (
  • handleSelect(entry)} > {entry.label}
  • ))}
)} {/* No results message */} {isOpen && !disabled && query && filteredEntries.length === 0 && (
No results found for "{query}"
)}
) }, ) SearchInput.displayName = "SearchInput"