

`useSearch` is the reusable search state hook for Cubitt. It can run in query-only mode or local results mode.

Use it when you want one place to manage:

* the live input value
* the debounced value
* clear/reset behavior
* optional URL sync
* optional includes or fuzzy matching against local items

## Query-Only Usage [#query-only-usage]

```tsx
import { Button, Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";

function ToolbarSearch() {
  const search = useSearch({
    debounceMs: 100,
    paramName: "q",
  });

  return (
    <SearchExpand expandedWidth="20rem">
      <Input placeholder="Search..." {...search.inputProps} />
      <Button
        disabled={search.isEmpty}
        mode="icon"
        onClick={search.clear}
        size="sm"
        variant="link"
      >
        {!search.isEmpty && <Xmark />}
      </Button>
    </SearchExpand>
  );
}
```

In this mode, `useSearch` manages input state, debounce, and URL synchronization but does not calculate local results.

## Local Includes Search [#local-includes-search]

```tsx
const search = useSearch({
  items,
  keys: ["name", "email"],
  debounceMs: 100,
});

const visibleItems = search.results ?? items;
```

The default strategy is `"includes"`, which does a case-insensitive substring match across the configured keys.

## Local Fuzzy Search [#local-fuzzy-search]

```tsx
const search = useSearch({
  strategy: "fuzzy",
  items,
  keys: ["name", "email", (item) => item.department],
  debounceMs: 100,
  paramName: "q",
});

const visibleItems = search.results ?? items;
```

`search.results` is already ordered by relevance. If you need metadata as well, use `search.rankedResults`.

## DataTable Integration [#datatable-integration]

Use `useSearch` outside the table and pass the filtered rows into `DataTable`.

```tsx
const search = useSearch<Row>({
  strategy: "fuzzy",
  items: data,
  keys: ["name", "email", "department"],
  debounceMs: 100,
});

<DataTable
  columns={columns}
  data={search.results ?? data}
  enableSorting
/>
```

This keeps search reusable and avoids a table-specific search component.

## API [#api]

### Options [#options]

| Option                | Type                                      | Default      | Description                                          |
| --------------------- | ----------------------------------------- | ------------ | ---------------------------------------------------- |
| `value`               | `string`                                  | -            | Controlled search value                              |
| `defaultValue`        | `string`                                  | `""`         | Initial value for uncontrolled mode                  |
| `onValueChange`       | `(value: string) => void`                 | -            | Called when the value changes                        |
| `debounceMs`          | `number`                                  | `0`          | Debounce delay before `debouncedValue` updates       |
| `items`               | `readonly TItem[]`                        | -            | Optional local dataset to search                     |
| `strategy`            | `"includes" \| "fuzzy" \| "custom"`       | `"includes"` | Search strategy to apply                             |
| `keys`                | `SearchKey<TItem>[]`                      | -            | Keys or selectors used to derive searchable values   |
| `matcher`             | `(item: TItem, query: string) => boolean` | -            | Required when `strategy` is `"custom"`               |
| `limit`               | `number`                                  | -            | Maximum number of returned results                   |
| `minQueryLength`      | `number`                                  | `0`          | Minimum trimmed query length before filtering starts |
| `paramName`           | `string`                                  | -            | URL query parameter key                              |
| `paramClearOnDefault` | `boolean`                                 | `true`       | Remove the param when the value matches the default  |
| `paramThrottle`       | `number`                                  | -            | Throttle URL updates                                 |
| `paramDebounce`       | `number`                                  | -            | Debounce URL updates                                 |
| `onUrlValueChange`    | `(value: string \| null) => void`         | -            | Callback when the URL value changes                  |

### Return Value [#return-value]

| Field            | Type                                 | Description                                     |
| ---------------- | ------------------------------------ | ----------------------------------------------- |
| `value`          | `string`                             | Immediate input value                           |
| `debouncedValue` | `string`                             | Debounced query value                           |
| `setValue`       | `(value: string) => void`            | Imperatively update the query                   |
| `clear`          | `() => void`                         | Reset the query to an empty string              |
| `isEmpty`        | `boolean`                            | `true` when `value === ""`                      |
| `hasQuery`       | `boolean`                            | `true` when the trimmed value is non-empty      |
| `inputProps`     | `{ value; onChange }`                | Controlled props for a composed input           |
| `results`        | `TItem[] \| undefined`               | Ordered visible items when `items` was provided |
| `rankedResults`  | `SearchResult<TItem>[] \| undefined` | Ordered results plus rank metadata              |

## Notes [#notes]

* When `useSearch` owns URL sync, do not also pass URL-state props directly to the composed `Input`.
* `useSearch` does not render UI. It is designed to compose with `SearchExpand`, `Input`, `Autocomplete`, or custom inputs.
* `matcher` is only used for `"custom"` strategy.
