<!-- source-hash: 8ccc6891a2b3966ffa91a4330f0065e9 -->
Generic React hook for managing debounced async search state, including loading, error handling, and result mapping.

## Key Components

### `UseSearchConfig<T>`
Configuration interface for the hook:
- `searchFn` — async function that receives the query string and returns raw results
- `mapResult` — transforms each raw result into a `SearchResult`
- `debounceMs` — debounce delay in milliseconds (default: `300`)
- `minQueryLength` — minimum characters before triggering a search (default: `2`)

### `UseSearchReturn`
Return shape exposing:
- `query` / `setQuery` — controlled input state
- `results` — mapped `SearchResult[]`
- `isLoading` — `true` while the async search is in flight
- `error` — error message string or `null`
- `clearResults` — resets results and error state

### `useSearch<T>(config)`
Core hook that orchestrates debouncing via `useDebounce`, fires `searchFn` when the debounced query meets `minQueryLength`, and cancels stale in-flight requests via a `cancelled` flag on cleanup.

## Usage Example

```typescript
import { useSearch } from "@/hooks/use-search"
import type { Device } from "@/types"

const { query, setQuery, results, isLoading, error } = useSearch<Device>({
  searchFn: async (q) => await api.devices.search(q),
  mapResult: (device) => ({
    id: device.id,
    label: device.hostname,
    description: device.ipAddress,
  }),
  debounceMs: 400,
  minQueryLength: 3,
})

// Bind to an input
<SearchInput
  value={query}
  onChange={setQuery}
  results={results}
  loading={isLoading}
  error={error}
/>
```

> Stale request cancellation is handled automatically — if the query changes before a previous search resolves, its result is silently discarded.