'use client'; /* ── SearchInput ── * * Controlled search input with search icon, debounced search, * loading spinner, clear button, and keyboard navigation handler. */ import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react"; import { Search, X, Loader2 } from "lucide-react"; /* ── Types ── */ export interface SearchInputHandle { clear: () => void; focus: () => void; blur: () => void; } interface SearchInputProps { /** Called (debounced) when the user types */ onSearch: (query: string) => void; /** Keyboard navigation handler (from parent orchestrator) */ onKeyDown: (e: React.KeyboardEvent) => void; /** Called when input receives focus */ onFocus?: () => void; /** Whether a search is in progress */ loading: boolean; /** Whether there are results to show on focus */ hasResults: boolean; /** Placeholder text */ placeholder?: string; /** Auto-focus on mount */ autoFocus?: boolean; } /* ── Component ── */ const SearchInput = forwardRef(function SearchInput( { onSearch, onKeyDown, onFocus, loading, hasResults, placeholder = "Search files, CIDs, and content…", autoFocus = false }, ref, ) { const [query, setQuery] = useState(""); const inputRef = useRef(null); const debounceRef = useRef | null>(null); /* Expose clear + focus to parent */ useImperativeHandle(ref, () => ({ clear() { setQuery(""); }, focus() { inputRef.current?.focus(); }, blur() { inputRef.current?.blur(); }, })); /* ── Debounced input ── */ function handleChange(value: string) { setQuery(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => onSearch(value), 250); } /* ── Cleanup ── */ useEffect(() => { if (autoFocus) inputRef.current?.focus(); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [autoFocus]); /* ── Render ── */ return (
handleChange(e.target.value)} onKeyDown={onKeyDown} onFocus={() => { if (hasResults) onFocus?.(); }} placeholder={placeholder} className="w-full pl-10 pr-10 py-2.5 rounded-xl bg-surface-900 border border-surface-700 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/50 focus-visible:border-brand-500 transition-colors" autoFocus={autoFocus} role="combobox" aria-expanded={hasResults} aria-haspopup="listbox" aria-autocomplete="list" aria-controls="search-results-listbox" /> {loading && ( )} {!loading && query && ( )}
); }); export default SearchInput;