

## Overview [#overview]

`DataTable` does not own search state. `useSearch` owns the query, debounce, matching strategy, and result ordering. The usual pattern is:

* `useSearch` owns query state and local matching
* your input UI reads from `search.inputProps`
* `DataTable` receives `search.results ?? data`

<Tabs className="bg-transparent border-none rounded-none" items="['Preview', 'Code']">
  <Tab value="Preview" className="p-0">
    <Preview name="SearchOnlyExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { DataTable } from "@tilt-legal/cubitt-components/composites";
    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 SearchableTable({ data, columns }: Props) {
      const search = useSearch<Row>({
        items: data,
        keys: ["name", "email", "department"],
        strategy: "fuzzy",
        debounceMs: 100,
      });

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

          <DataTable columns={columns} data={search.results ?? data} />
        </>
      );
    }
    ```
  </Tab>
</Tabs>

`DataTable` never performs the matching itself. It receives rows that `useSearch` has already filtered and ordered.

## Matching Modes [#matching-modes]

`useSearch` supports different matching strategies depending on how exact or forgiving the search should feel.

### Includes Search [#includes-search]

`"includes"` is the default. It performs a case-insensitive substring match across the configured keys.

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

Use this when you want predictable string matching and stable original ordering.

### Fuzzy Search and Relevance [#fuzzy-search-and-relevance]

Use `"fuzzy"` when you want typo-tolerant matching and relevance ordering.

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

const rows = search.results ?? data;
```

With fuzzy search, `search.results` is already ordered by relevance. If you also need explicit rank metadata, use `search.rankedResults`.

```tsx
search.rankedResults?.map((result) => ({
  row: result.item,
  rank: result.rank,
}));
```

## Search With Column Filters [#search-with-column-filters]

Treat search as the first pass, then let structured column filters run on the already matched rows.

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

const filterDefs = defineFilters([
  {
    key: "status",
    label: "Status",
    type: "select",
  },
  {
    key: "department",
    label: "Department",
    type: "select",
  },
] as const);

const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns: tableColumns,
  data: search.results ?? data,
  strategy: "client",
});

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 /> : null}
      </Button>
    </SearchExpand>

<Filters
  fields={tableFilters.filterFields}
  filters={tableFilters.filters}
  onChange={tableFilters.handleFiltersChange}
/>
<DataTable
  columnFilters={tableFilters.columnFilters}
  columns={tableFilters.columns}
  data={search.results ?? data}
  enableFiltering
  onColumnFiltersChange={tableFilters.onColumnFiltersChange}
/>
  </>
);
```

This keeps free-text search separate from the column-filter model while still composing cleanly into the same table.

## API Reference [#api-reference]

### Common `useSearch` Options [#common-usesearch-options]

| Option           | Type                                | Description                                          |
| ---------------- | ----------------------------------- | ---------------------------------------------------- |
| `items`          | `T[]`                               | Source rows to search locally                        |
| `keys`           | `SearchKey<T>[]`                    | Keys or selectors searched for local matching        |
| `strategy`       | `"includes" \| "fuzzy" \| "custom"` | Search strategy. Defaults to `"includes"`            |
| `debounceMs`     | `number`                            | Delay query updates while typing                     |
| `minQueryLength` | `number`                            | Minimum trimmed query length before filtering starts |
| `limit`          | `number`                            | Maximum number of returned results                   |
| `paramName`      | `string`                            | Optional URL parameter used for query state          |

### Common Return Fields [#common-return-fields]

| Field            | Type                             | Description                                |
| ---------------- | -------------------------------- | ------------------------------------------ |
| `inputProps`     | object                           | Controlled props to spread into your input |
| `value`          | `string`                         | Immediate query value                      |
| `debouncedValue` | `string`                         | Debounced query value                      |
| `results`        | `T[] \| undefined`               | Filtered and ordered rows for rendering    |
| `rankedResults`  | `SearchResult<T>[] \| undefined` | Ranked matches with relevance metadata     |
| `clear`          | `() => void`                     | Clear the current query                    |
| `setValue`       | `(value: string) => void`        | Imperatively set the query                 |
| `isEmpty`        | `boolean`                        | Whether the current query string is empty  |
| `hasQuery`       | `boolean`                        | Whether the trimmed query is non-empty     |

<Callout type="info">
  See [`useSearch`](/hooks/core/use-search) for the full hook API. This page
  covers the DataTable composition pattern rather than the entire hook surface.
</Callout>
