diff --git a/app/components/SearchInput.tsx b/app/components/SearchInput.tsx
new file mode 100644
index 00000000..3a764222
--- /dev/null
+++ b/app/components/SearchInput.tsx
@@ -0,0 +1,485 @@
+/**
+ * SearchInput
+ *
+ * An accessible search input that follows the ARIA combobox pattern (WAI-ARIA
+ * 1.2 §5.9). Users can navigate suggestions with the arrow keys, select with
+ * Enter, and dismiss the listbox with Escape.
+ *
+ * Accessibility features
+ * ──────────────────────
+ * • Input carries role="combobox" with aria-expanded, aria-haspopup="listbox",
+ * aria-autocomplete="list", and aria-controls pointing at the listbox.
+ * • aria-activedescendant tracks the keyboard-highlighted option.
+ * • Each option has role="option" and aria-selected.
+ * • The listbox label ("Search suggestions") is announced when it appears.
+ * • A live region announces result counts so screen-reader users know when
+ * the list has changed.
+ * • The clear button is keyboard-reachable and has a descriptive aria-label.
+ * • Reduced-motion: the dropdown entrance animation respects
+ * `prefers-reduced-motion: reduce`.
+ *
+ * Keyboard interaction
+ * ────────────────────
+ * ArrowDown Move visual focus to the next option (wraps at end)
+ * ArrowUp Move visual focus to the previous option (wraps at start)
+ * Home Move to the first option
+ * End Move to the last option
+ * Enter Confirm the highlighted option (or submit raw query if none)
+ * Escape Close the listbox and return focus to the input
+ * Tab Close the listbox (natural focus movement)
+ */
+
+"use client";
+
+import React, {
+ useState,
+ useRef,
+ useId,
+ useMemo,
+ useCallback,
+ useEffect,
+} from "react";
+import { Search as SearchIcon, X as ClearIcon, Loader2 } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+/** A single suggestion item displayed in the listbox. */
+export interface SearchSuggestion {
+ /** Unique identifier for the item. */
+ id: string;
+ /** Primary label shown to the user. */
+ label: string;
+ /** Optional sub-label shown in a smaller font beneath the label. */
+ sublabel?: string;
+ /** Optional accessible icon rendered before the label (must carry aria-hidden). */
+ icon?: React.ReactNode;
+}
+
+export interface SearchInputProps {
+ /** Current value of the input (controlled). */
+ value?: string;
+ /** Called when the user changes the input value. */
+ onChange?: (value: string) => void;
+ /**
+ * Suggestions to display in the listbox. The consumer is responsible for
+ * filtering / sorting; SearchInput renders whatever is provided.
+ */
+ suggestions?: SearchSuggestion[];
+ /**
+ * Called when the user confirms a suggestion (Enter key or click).
+ * Receives the confirmed suggestion object.
+ */
+ onSelect?: (suggestion: SearchSuggestion) => void;
+ /**
+ * Called when the user submits a free-text query (Enter with no suggestion
+ * highlighted, or Enter when suggestions is empty).
+ */
+ onSubmit?: (query: string) => void;
+ /** Called when the listbox opens. */
+ onOpen?: () => void;
+ /** Called when the listbox closes. */
+ onClose?: () => void;
+ /** Show a spinner inside the input to indicate loading. */
+ isLoading?: boolean;
+ /** Placeholder text for the input. */
+ placeholder?: string;
+ /** Additional class names for the outermost wrapper. */
+ className?: string;
+ /**
+ * Maximum number of suggestions to show. Remaining items are hidden.
+ * @default 10
+ */
+ maxSuggestions?: number;
+ /** Whether the input should be disabled. */
+ disabled?: boolean;
+ /** id forwarded to the underlying . Useful for