

<Preview name="SearchExpandDefaultExample" />

## Overview [#overview]

The **SearchExpand** component renders as a compact icon button that smoothly expands into a search input when clicked. The icon from the button becomes the leading icon in the input. Ideal for toolbars and headers where space is constrained.

The expanded state uses the same composition pattern as `InputWrapper` — pass an `Input` and optional trailing elements (clear buttons, submit buttons, etc.) as children.

## Usage [#usage]

```tsx
import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
```

```tsx
<SearchExpand>
  <Input placeholder="Search..." size="md" variant="default" />
</SearchExpand>
```

<Accordions type="single">
  <Accordion title="URL State Management">
    For search surfaces, prefer `useSearch` as the state layer and let `SearchExpand` stay presentation-only:

    ```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 UrlSyncedSearch() {
      const search = useSearch({
        debounceMs: 100,
        paramName: "q",
        paramDebounce: 300,
      });

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

## Examples [#examples]

### Sizes [#sizes]

All three sizes match Button and Input height tokens exactly (sm=28px, md=34px, lg=40px). Pass the matching `size` to both `SearchExpand` and `Input`.

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

  <Tab value="Code">
    ```tsx
    import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";

    <div className="flex items-center gap-4">
      <SearchExpand size="sm">
        <Input placeholder="Small" size="sm" variant="default" />
      </SearchExpand>
      <SearchExpand size="md">
        <Input placeholder="Medium" size="md" variant="default" />
      </SearchExpand>
      <SearchExpand size="lg">
        <Input placeholder="Large" size="lg" variant="default" />
      </SearchExpand>
    </div>
    ```
  </Tab>
</Tabs>

### Button Variants [#button-variants]

The collapsed button supports `ghost`, `outline`, and `secondary` variants.

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

  <Tab value="Code">
    ```tsx
    import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";

    <div className="flex items-center gap-4">
      <SearchExpand variant="ghost">
        <Input placeholder="Ghost" size="md" variant="default" />
      </SearchExpand>
      <SearchExpand variant="outline">
        <Input placeholder="Outline" size="md" variant="default" />
      </SearchExpand>
      <SearchExpand variant="secondary">
        <Input placeholder="Secondary" size="md" variant="default" />
      </SearchExpand>
    </div>
    ```
  </Tab>
</Tabs>

### Custom Icon [#custom-icon]

Pass any Cubitt icon via the `icon` prop. It appears in both the collapsed button and expanded input.

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

  <Tab value="Code">
    ```tsx
    import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
    import { SearchExpand } from "@tilt-legal/cubitt-components/primitives";
    import {
      Filter } from "@tilt-legal/cubitt-icons/ui/outline";

    <SearchExpand icon={Filter}>
      <Input placeholder="Filter items..." size="md" variant="default" />
    </SearchExpand>
    ```
  </Tab>
</Tabs>

### Custom Width [#custom-width]

Control how wide the input expands with `expandedWidth`.

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

  <Tab value="Code">
    ```tsx
    import { Input } from "@tilt-legal/cubitt-components/primitives";

    <SearchExpand expandedWidth="12rem">
      <Input placeholder="Narrow (12rem)" size="md" variant="default" />
    </SearchExpand>
    <SearchExpand expandedWidth="24rem">
      <Input placeholder="Wide (24rem)" size="md" variant="default" />
    </SearchExpand>
    ```
  </Tab>
</Tabs>

### Clearable [#clearable]

Add a clear button as a trailing child — same pattern as `InputWrapper`.

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

  <Tab value="Code">
    ```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 ClearableSearch() {
      const search = useSearch({ debounceMs: 0 });

      return (
        <SearchExpand expandedWidth="20rem">
          <Input
            placeholder="Type to search..."
            size="md"
            variant="default"
            {...search.inputProps}
          />
          <Button
            className="-me-0.5 size-5"
            disabled={search.isEmpty}
            mode="icon"
            onClick={search.clear}
            size="sm"
            variant="link"
          >
            {!search.isEmpty && <Xmark />}
          </Button>
        </SearchExpand>
      );
    }
    ```
  </Tab>
</Tabs>

### Controlled [#controlled]

Use `expanded` and `onExpandedChange` for full control over the expand/collapse state.

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

  <Tab value="Code">
    ```tsx
    import { Input } from "@tilt-legal/cubitt-components/primitives";
    import { SearchExpand } from "@tilt-legal/cubitt-components/primitives";
    import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";
    import { useState } from "react";

    function ControlledSearch() {
      const [expanded, setExpanded] = useState(false);
      const search = useSearch({ debounceMs: 0 });

      return (
        <SearchExpand
          expanded={expanded}
          onExpandedChange={setExpanded}
          expandedWidth="20rem"
        >
          <Input
            placeholder="Controlled search..."
            size="md"
            variant="default"
            {...search.inputProps}
          />
        </SearchExpand>
      );
    }
    ```
  </Tab>
</Tabs>

### URL State [#url-state]

SearchExpand with URL state synchronization owned by `useSearch`.

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

  <Tab value="Code">
    ```tsx
    import { Input, SearchExpand } from "@tilt-legal/cubitt-components/primitives";
    import { useSearch } from "@tilt-legal/cubitt-components/utilities/hooks";

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

      return (
        <SearchExpand expandedWidth="20rem">
          <Input
            placeholder="Type to update URL"
            size="md"
            variant="default"
            {...search.inputProps}
          />
        </SearchExpand>
      );
    }
    ```
  </Tab>
</Tabs>

## Keyboard & Focus [#keyboard--focus]

* **Click** or **Enter/Space** on the collapsed button expands the input and auto-focuses it.
* **Escape** collapses the input and returns focus to the button.
* **Blur** when the input is empty collapses it automatically (configurable via `collapseOnBlurWhenEmpty`).
* **Blur** when the input has a value keeps it expanded.

***

## API Reference [#api-reference]

### SearchExpand [#searchexpand]

The outer container that handles the expand/collapse animation and button state. Pass `Input` and optional trailing elements as children.

| Prop                      | Type                                  | Default       | Description                                                       |
| ------------------------- | ------------------------------------- | ------------- | ----------------------------------------------------------------- |
| `children`                | `ReactNode`                           | —             | Content shown when expanded (Input + optional trailing elements)  |
| `icon`                    | `CubittIcon`                          | `Magnifier`   | Icon shown in both collapsed and expanded states                  |
| `size`                    | `"sm" \| "md" \| "lg"`                | `"md"`        | Matches Button/Input height tokens                                |
| `expandedWidth`           | `string \| number`                    | `"16rem"`     | Width when expanded                                               |
| `variant`                 | `"ghost" \| "outline" \| "secondary"` | `"secondary"` | Button variant for the collapsed state                            |
| `expanded`                | `boolean`                             | —             | Controlled expanded state                                         |
| `onExpandedChange`        | `(expanded: boolean) => void`         | —             | Callback when expanded state changes                              |
| `collapseOnBlurWhenEmpty` | `boolean`                             | `true`        | Collapse on blur when input is empty                              |
| `collapseOnEscape`        | `boolean`                             | `true`        | Collapse when Escape is pressed                                   |
| `label`                   | `string`                              | `"Search"`    | Accessible label for the button and input                         |
| `className`               | `string`                              | —             | Additional className on the outer container                       |
| `reduceMotion`            | `boolean`                             | `false`       | Disable animations (respects `prefers-reduced-motion` by default) |
| `disabled`                | `boolean`                             | `false`       | Disable the component                                             |

### Children [#children]

SearchExpand uses the same composition pattern as `InputWrapper`. Pass an `Input` as the primary child and optional trailing elements:

```tsx
<SearchExpand>
  <Input placeholder="Search..." size="md" variant="default" />
  <Button mode="icon" size="sm" variant="link" onClick={onClear}>
    <Xmark />
  </Button>
</SearchExpand>
```

Input props such as `placeholder`, `size`, and `variant` are set directly on the `Input` child. For search state, prefer `useSearch` and pass `search.inputProps` into the composed input. If `useSearch` owns URL sync, do not also pass `paramName` to the `Input` child.
