"use client"; import { useEffect, useRef, useState, useId } from "react"; import { useRouter } from "next/navigation"; import { Search, CornerDownLeft } from "lucide-react"; type Hit = { slug: string; name: string; summary: string; kind: "tool" | "category" }; export function SearchBox({ size = "lg", autoFocus = false, defaultValue = "", ariaLabel }: { size?: "sm" | "lg"; autoFocus?: boolean; defaultValue?: string; ariaLabel?: string }) { const router = useRouter(); const id = useId(); const [q, setQ] = useState(defaultValue); const [hits, setHits] = useState([]); const [open, setOpen] = useState(false); const [active, setActive] = useState(-1); const box = useRef(null); const input = useRef(null); const timer = useRef | null>(null); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "/" && !/input|textarea/i.test((e.target as HTMLElement).tagName)) { e.preventDefault(); input.current?.focus(); } }; const onClick = (e: MouseEvent) => { if (!box.current?.contains(e.target as Node)) setOpen(false); }; window.addEventListener("keydown", onKey); window.addEventListener("click", onClick); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("click", onClick); }; }, []); useEffect(() => { if (timer.current) clearTimeout(timer.current); if (q.trim().length < 2) { setHits([]); return; } timer.current = setTimeout(async () => { try { const r = await fetch(`/api/suggest?q=${encodeURIComponent(q.trim())}`); if (r.ok) { setHits(await r.json()); setOpen(true); setActive(-1); } } catch {} }, 120); }, [q]); const go = (h?: Hit) => { setOpen(false); if (h) router.push(h.kind === "tool" ? `/tools/${h.slug}` : `/categories/${h.slug}`); else if (q.trim()) router.push(`/tools?q=${encodeURIComponent(q.trim())}`); }; const onKey = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setOpen(true); setActive((a) => Math.min(hits.length - 1, a + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(-1, a - 1)); } else if (e.key === "Enter") { e.preventDefault(); go(active >= 0 ? hits[active] : undefined); } else if (e.key === "Escape") { setOpen(false); input.current?.blur(); } }; const lg = size === "lg"; return (
{ e.preventDefault(); go(); }} className="relative"> setQ(e.target.value)} onFocus={() => hits.length && setOpen(true)} onKeyDown={onKey} autoFocus={autoFocus} type="search" name="q" autoComplete="off" spellCheck={false} placeholder={lg ? "Search tools, e.g. vector database, LaTeX, intraday data…" : "Search tools…"} aria-label="Search tools" role="combobox" aria-expanded={open} aria-controls={`${id}-list`} aria-autocomplete="list" aria-activedescendant={active >= 0 ? `${id}-opt-${active}` : undefined} className={`input ${lg ? "h-14 rounded-2xl pl-12 pr-24 text-base shadow-soft" : "h-9 rounded-xl pl-9 pr-12 text-sm"}`} /> {open && hits.length > 0 && (
    {hits.map((h, i) => (
  • setActive(i)} onMouseDown={(e) => { e.preventDefault(); go(h); }} className={`flex cursor-pointer items-start gap-3 rounded-xl px-3 py-2 ${i === active ? "bg-bg-3" : ""}`}> {h.kind} {h.name}{h.summary} {i === active && }
  • ))}
  • Enter to search all results · ↑↓ to navigate
)}
); }