# Combobox
URL: https://remocn.dev/docs/ui/components/combobox

A searchable select with a typed-query input, filtered panel, and row selection

- Install: `npx shadcn@latest add @remocn/combobox`
- Vibe: clean
- Natural length: 120f @ 30fps

A state atom representing a searchable select: a text-input trigger and a panel of filtered options. Its appearance (`opened`, `closed`) is a pure function of the `state` prop. Pass a state directly or drive it from the Remotion timeline via `useCurrentState`. The component reads no frame — only the state value you give it. The panel reveals below the trigger with opacity, scale, and vertical lift animations. The trigger reuses the Input visual presets; the panel rows reuse the SelectItem atom.

## Installation

```bash
npx shadcn@latest add @remocn/combobox
```

> **Note**
>
> Installing `combobox` automatically installs the shared `remocn-ui` core (`lib/remocn-ui/`), the `input` primitive, and the `select-item` atom via `registryDependencies` (`["remocn-ui", "input", "select-item"]`). You do not need to install them separately. The trigger renders from Input's visual presets, and the panel renders a column of SelectItem rows.

## States

`ComboboxState` is:

```ts
type ComboboxState =
  | "opened"  // panel opacity 1, scale 1, translateY 0
  | "closed"  // panel opacity 0, scale 0.96, translateY -4px
```

`opened` reveals the dropdown panel below the trigger. `closed` hides it.

## Snap usage

Pass `state` directly — the component snaps instantly to that visual. Useful for static previews or when you drive state from your own logic:

```tsx
import { Combobox } from "@/components/remocn/combobox";

export const Scene = () => (
  <div style={{ position: "relative", width: "100%", height: "100%" }}>
    <Combobox state="opened" query="ba" />
  </div>
);
```

To drive state from the timeline, use `useCurrentState`:

```tsx
import { useCurrentState } from "@/lib/remocn-ui";
import { Combobox } from "@/components/remocn/combobox";

export const Scene = () => {
  const state = useCurrentState(
    [
      { at: 16, state: "opened" },
      { at: 96, state: "closed" },
    ],
    "closed",
  );

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox state={state} />
    </div>
  );
};
```

Each `at` is a **Sequence-local authored frame**. State persists between steps. See [Concepts](/docs/ui/concepts) for the full `useCurrentState` API.

## Smooth transitions

State changes via `state` snap with no cross-fade. For an animated open/close, use `useComboboxTransition` from the copied `use-combobox-transition.ts` file. It reads the frame, interpolates between state presets, and returns a resolved `ComboboxStyle` — pass it to the `style` prop:

```tsx
import { Combobox } from "@/components/remocn/combobox";
import { useComboboxTransition } from "@/components/remocn/use-combobox-transition";

export const Scene = () => {
  const style = useComboboxTransition([
    { at: 16, state: "opened", duration: 12 },
    { at: 96, state: "closed", duration: 12 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox style={style} />
    </div>
  );
};
```

The `duration` field on each step overrides the file's `DEFAULT_DURATION` (= `12`) for that specific transition. To globally tune timing and easing, edit `use-combobox-transition.ts` directly in your project — that file is yours (shadcn "own your code" philosophy).

`style` takes precedence over `state` when both are provided.

## Typing and filtering

The `query` prop holds the full search string. The `revealCount` prop controls how many characters of `query` are currently shown — the panel filters the option list against the visible prefix and shows a "No results found." empty state when nothing matches.

Use `revealCount` from `@/lib/remocn-ui` to compute the reveal from the timeline frame:

```tsx
import { useCurrentFrame } from "remotion";
import { revealCount } from "@/lib/remocn-ui";
import { Combobox } from "@/components/remocn/combobox";
import { useComboboxTransition } from "@/components/remocn/use-combobox-transition";

const QUERY = "ba";
const TYPE_START = 32;

export const Scene = () => {
  const frame = useCurrentFrame();

  const style = useComboboxTransition([
    { at: 16, state: "opened", duration: 12 },
    { at: 96, state: "closed", duration: 12 },
  ]);

  // 2 chars at 4 chars/sec @ 30 fps = 15 frames total typing window.
  const revealed = revealCount(
    Math.max(0, frame - TYPE_START), // typing starts at frame 32
    30,                               // fps
    QUERY.length,                     // total characters
    4,                                // chars per second
  );

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <Combobox style={style} query={QUERY} revealCount={revealed} />
    </div>
  );
};
```

`revealCount(localFrame, fps, len, cps)` is a pure function — it returns the number of characters to show given the elapsed local frame, fps, total query length, and typing speed in chars per second. Import it from `@/lib/remocn-ui`.

When `revealCount` is omitted, the full `query` is shown immediately. When the visible prefix is empty, all items are shown. The pure filter is exported as `filterComboboxItems(items, query, revealCount)` if you need it in your own logic.

## Items

The panel renders a column of **SelectItem** rows — one per filtered option. The row visual is the same SelectItem atom used by the Select and Dropdown Menu components. See the [Select page](/docs/ui/components/select#items) for the full `SelectItemState` documentation (`idle`, `hover`, `press`, `selected`) and the `SelectItem` atom preview.

When you install `combobox`, the `select-item` atom is automatically pulled in via `registryDependencies` — no separate install needed. The `Combobox` container derives each row's state from `selectedIndex`, `highlightedIndex`, and `pressedIndex` (all indexed into the **filtered** list), or you can override a row's visual with `itemStyles`.

To animate a row's state transition, drive it with `useSelectItemTransition`:

```tsx
import { useSelectItemTransition } from "@/components/remocn/use-select-item-transition";

const itemStyle = useSelectItemTransition([
  { at: 60, state: "hover",    duration: 8 },
  { at: 72, state: "press",    duration: 6 },
  { at: 80, state: "selected", duration: 8 },
]);

// Pass to Combobox via itemStyles (indexed into the FILTERED list):
<Combobox style={panelStyle} itemStyles={[itemStyle]} />
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `state` | `"opened" \| "closed"` | `"closed"` | Current visual state (snap path). State changes snap — no automatic cross-fade. |
| `style` | `ComboboxStyle` | — | Resolved animated visual (smooth path). Pass an interpolated ComboboxStyle from useComboboxTransition. Takes precedence over state when provided. |
| `query` | `string` | `""` | Full search query text. The visible prefix is query.slice(0, revealCount). The item list is filtered against this prefix. |
| `revealCount` | `number` | — | Number of query characters revealed so far. Omitted means the whole query is shown immediately. Use the revealCount() helper from @/lib/remocn-ui to compute this from the frame. |
| `placeholder` | `string` | `"Select a fruit…"` | Trigger placeholder shown before any query is typed. |
| `items` | `string[]` | `["Apple", "Banana", "Orange", "Grape"]` | The full option list before filtering. Each entry is a plain string. |
| `selectedIndex` | `number` | `-1` | Index into the FILTERED list of the persisted selection. -1 for none. |
| `highlightedIndex` | `number` | `-1` | Index into the FILTERED list of the hovered row. -1 for none. |
| `pressedIndex` | `number` | `-1` | Index into the FILTERED list of the pressed row. -1 for none. |
| `itemStyles` | `(SelectItemStyle \| undefined)[]` | — | Per-row resolved visual override (smooth path), indexed into the FILTERED list. When present it wins over the index-to-state derivation. Drive individual rows with useSelectItemTransition. |
| `inputStyle` | `InputStyle` | — | Resolved Input visual for the trigger (smooth path). Drive it with useInputTransition for a focus-ring + typed-text reveal animation. Defaults to the typing look once a query is revealed, else idle. |
| `theme` | `Partial<RemocnTheme>` | — | Per-component theme override. Merges with the active RemocnUIProvider theme and the mode defaults. Must be concrete oklch/hex/rgb values — not CSS custom properties. |
| `mode` | `"light" \| "dark"` | `"light"` | Sets the base palette. Overrides the mode on RemocnUIProvider when both are present. |
| `className` | `string` | — | Optional className applied to the outer wrapper element. |

## Use when
- Showing a searchable/filterable dropdown with a typed query narrowing the list on the timeline.
- Demoing autocomplete or type-ahead selection UX in a product walkthrough.
- A form field needs a constrained set of options but with search, not free-form text.

## Don't use when
- The list is not filterable and selection is direct — use `select` (non-filterable dropdown) instead.
- The overlay is a command palette with icons and keyboard shortcuts — use `command-menu` instead.
- The input is free-form text entry with no list — use `input` instead.