

<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 } from "@tilt-legal/cubitt-components/input";
import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
```

```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 } from "@tilt-legal/cubitt-components/button";
    import { Input } from "@tilt-legal/cubitt-components/input";
    import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
    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>
          <Input placeholder="Search" size="md" variant="default" {...search.inputProps} />
          <Button
            aria-label="Clear search"
            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]

### States [#states]

Disabled uses the collapsed button state, while error and readonly are shown in expanded input mode.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandStatesExample" />
  </Tab>

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

    const states = ["default", "error", "disabled", "readonly"] as const;
    const [state, setState] =
      useState<"default" | "error" | "disabled" | "readonly">("default");

    <SearchExpand
      disabled={state === "disabled"}
      expanded={state === "error" ? true : undefined}
      readOnly={state === "readonly"}
    >
      <Input
        aria-invalid={state === "error" ? true : undefined}
        disabled={state === "disabled"}
        placeholder={state === "readonly" ? "Readonly" : state}
        readOnly={state === "readonly"}
        value={state === "readonly" ? "Readonly search" : undefined}
      />
    </SearchExpand>;
    ```
  </Tab>
</Tabs>

### 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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandSizesExample" />
  </Tab>

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

    <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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandVariantsExample" />
  </Tab>

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

    <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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandCustomIconExample" />
  </Tab>

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

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

### Tooltip [#tooltip]

Use `tooltip` to label the collapsed button state when the icon needs extra context.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandTooltipExample" />
  </Tab>

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

    <div className="flex items-center gap-4">
      <SearchExpand tooltip={{ content: "Search matters", side: "bottom" }}>
        <Input placeholder="Search matters..." size="md" variant="default" />
      </SearchExpand>
      <SearchExpand
        disabled
        tooltip={{ content: "Search is unavailable", side: "bottom" }}
      >
        <Input disabled placeholder="Search locked..." size="md" variant="default" />
      </SearchExpand>
    </div>
    ```
  </Tab>
</Tabs>

### Custom Width [#custom-width]

Control how wide the input expands with `expandedWidth`.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandWidthExample" />
  </Tab>

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

    <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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandClearableExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Button } from "@tilt-legal/cubitt-components/button";
    import { Input } from "@tilt-legal/cubitt-components/input";
    import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
    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>
          <Input
            placeholder="Type to search..."
            size="md"
            variant="default"
            {...search.inputProps}
          />
          <Button
            aria-label="Clear search"
            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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandControlledExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Input } from "@tilt-legal/cubitt-components/input";
    import { SearchExpand } from "@tilt-legal/cubitt-components/search-expand";
    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}
        >
          <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 items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SearchExpandURLStateExample" />
  </Tab>

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

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

      return (
        <SearchExpand>
          <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                         |
| `tooltip`                 | `ButtonTooltip`                       | —             | Tooltip shown for the collapsed button state                      |
| `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                                             |
| `readOnly`                | `boolean`                             | `false`       | Force the expanded input presentation without allowing collapse   |

### 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
    aria-label="Clear search"
    mode="icon"
    onClick={onClear}
    size="sm"
    variant="link"
  >
    <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.
