'use client' import * as React from 'react' import { cn } from '../../utils/cn' import { useDebounce } from '../../hooks/ui/use-debounce' import { SearchIcon } from '../icons-v2-generated' export interface ChatHeaderSearchFieldProps { /** Seeds the field on mount (the current server-side search term). */ initialValue?: string /** Emits the DEBOUNCED term. The host owns the query and refetches the * dialog list server-side — the field never filters locally. */ onSearchChange: (query: string) => void /** Collapse the field back to the title. Fired on Escape and on blur while * the field is empty (an accidental open closes itself). */ onCollapse?: () => void /** Focus the input on mount (default true — the field only mounts when the * user opens search, so we drop them straight into typing). */ autoFocus?: boolean /** Appended to the root element (e.g. to tweak the open animation). */ className?: string } /** * Inline chat-header search field (Figma node 116:51217). When search is * toggled on, the panel header's title area is REPLACED in place by this * full-height field — a leading magnifier + a bare, borderless input — instead * of dropping a separate search bar into the list body. Holds its own text for * snappy typing and emits the debounced term via `onSearchChange`. */ export function ChatHeaderSearchField({ initialValue, onSearchChange, onCollapse, autoFocus = true, className, }: ChatHeaderSearchFieldProps) { const [value, setValue] = React.useState(initialValue ?? '') const debounced = useDebounce(value, 300) const lastEmitted = React.useRef(initialValue ?? '') // Exit animation: collapsing plays `animate-out` (slide/fade back toward the // right) BEFORE the parent unmounts us. We stay mounted through the leave — // the parent only drops the field once we call `onCollapse`, which we defer // to the animation's end. const [exiting, setExiting] = React.useState(false) const collapsedRef = React.useRef(false) React.useEffect(() => { // Once the collapse starts, no further emits: Escape already flushed the // cleared term synchronously, and a still-pending debounce of the typed // text ("abc" → Escape before 300ms) must not resurrect a stale query // while the field animates out. if (exiting) return if (debounced === lastEmitted.current) return lastEmitted.current = debounced onSearchChange(debounced) }, [debounced, exiting, onSearchChange]) const finishCollapse = React.useCallback(() => { if (collapsedRef.current) return collapsedRef.current = true onCollapse?.() }, [onCollapse]) const beginCollapse = React.useCallback(() => { setExiting(true) }, []) // Fallback for when `animationend` never fires (reduced motion, or the // animation utilities are disabled) — otherwise the field would hang open. React.useEffect(() => { if (!exiting) return const t = setTimeout(finishCollapse, 240) return () => clearTimeout(t) }, [exiting, finishCollapse]) return (