# Sequence Recording Guide

TanStack Hotkeys provides the `HotkeySequenceRecorderController` for building UIs where users record multi-chord sequences (Vim-style shortcuts). Each step is captured like a single hotkey chord; users finish with Enter by default, or you can use manual commit and optional idle timeout.

Sequence recording uses `recordBy: 'code'` by default, preserving every step as a physical string such as `['[KeyG]', 'Alt+[KeyS]']`. Set `recordBy: 'key'` for logical characters. The shared rejection and conflict options follow the [hotkey recording guide](./hotkey-recording.md#validation-and-conflicts).

## Basic usage

```ts
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { HotkeySequenceRecorderController, formatForDisplay } from '@tanstack/lit-hotkeys'
import type { HotkeySequence } from '@tanstack/lit-hotkeys'

@customElement('sequence-recorder')
class SequenceRecorder extends LitElement {
  private recorder = new HotkeySequenceRecorderController(this, {
    onRecord: (sequence: HotkeySequence) => {
      console.log('Recorded:', sequence) // e.g., ["G", "G"]
    },
  })

  render() {
    const { isRecording, steps, recordedSequence } = this.recorder
    return html`
      <div>
        <button
          @click=${() =>
            isRecording
              ? this.recorder.cancelRecording()
              : this.recorder.startRecording()}
        >
          ${isRecording
            ? 'Press chords, then Enter…'
            : recordedSequence
              ? recordedSequence.map((h) => formatForDisplay(h)).join(' ')
              : 'Click to record'}
        </button>
        ${isRecording
          ? html`<button @click=${() => this.recorder.cancelRecording()}>
              Cancel
            </button>`
          : nothing}
      </div>
    `
  }
}
```

## Controller API

`HotkeySequenceRecorderController` exposes the following reactive getters and methods:

| Member | Type | Description |
|--------|------|-------------|
| `isRecording` | `boolean` | Whether the recorder is currently listening |
| `steps` | `HotkeySequence` | Chords captured in the current session |
| `recordedSequence` | `HotkeySequence \| null` | Last committed sequence |
| `startRecording()` | `() => void` | Start a new recording session |
| `stopRecording()` | `() => void` | Stop without calling `onRecord` |
| `cancelRecording()` | `() => void` | Stop and call `onCancel` |
| `commitRecording()` | `() => void` | Commit current `steps` (no-op if empty) |
| `setOptions(opts)` | `(Partial<HotkeySequenceRecorderOptions>) => void` | Update options at runtime |

The controller registers itself with the host in its constructor, subscribes to the underlying `HotkeySequenceRecorder` store on `hostConnected`, and cleans up on `hostDisconnected`.

## Options

`recordBy` defaults to `'code'`; choose `'key'` for logical characters. Repeats and IME composition never append steps. Modifier-only presses wait for a complete chord.

Pass options as the second argument to the constructor:

```ts
new HotkeySequenceRecorderController(this, {
  onRecord: (sequence) => { /* called when a sequence is committed */ },
  onCancel: () => { /* called when recording is cancelled */ },
  onClear: () => { /* called when cleared via Backspace with no steps */ },
  commitKeys: 'enter', // or 'none'
  idleTimeoutMs: 2000,
})
```

### `onRecord`

Called when a nonempty sequence is committed. Clearing calls only `onClear`. Receives the recorded `HotkeySequence` array.

### `onCancel`

Called when recording is cancelled (either by pressing Escape or calling `cancelRecording()`).

### `onClear`

Called when the sequence is cleared (by pressing Backspace or Delete during recording when no steps remain).

### `commitKeys`

Controls how the user finishes recording from the keyboard:

- `'enter'` (default): plain Enter (no modifiers) commits when at least one step exists.
- `'none'`: only `commitRecording()` or `idleTimeoutMs` finishes recording; plain Enter can be recorded as a chord.

### `commitOnEnter`

When `commitKeys` is `'enter'`, set to `false` to treat Enter as a normal chord. Use `commitRecording()` or idle timeout to finish instead.

### `idleTimeoutMs`

Milliseconds of inactivity after the last completed chord to auto-commit. The timer does not run while waiting for the first chord.

### `ignoreInputs`

The `HotkeySequenceRecorderOptions` supports an `ignoreInputs` option (defaults to `true`). When `true`, the recorder doesn't intercept normal typing in text inputs, textareas, selects, or contentEditable elements. Keystrokes pass through to the input as usual. Pressing Escape still cancels recording even when focused on an input. Set `ignoreInputs: false` if you want the recorder to capture keys from within input elements.

```ts
new HotkeySequenceRecorderController(this, {
  ignoreInputs: false, // record even from inside inputs
  onRecord: (sequence) => console.log(sequence),
})
```

## Recording behavior

| Input | Behavior |
|-------|----------|
| Valid chord | Appended to `steps`; listener stays active |
| Enter (no modifiers), `commitKeys: 'enter'`, `steps.length >= 1` | Commits and calls `onRecord` |
| Escape | Cancels; calls `onCancel` |
| Backspace / Delete (no modifiers) | Removes last step, or if empty runs only `onClear` and stops |

Recorded chords use portable `Mod` format, same as `HotkeyRecorderController`.

## Validation and conflicts

Sequence `validate(sequence, { events, parsedSequence })` runs when committing and returns true, false, or a rejection message. `detectConflicts` checks sequence prefixes as well as single bindings. Rejected commits keep recording active with the steps intact so the user can edit them with Backspace. Empty Backspace/Delete clears and emits `onClear` only; removing a nonempty step does not commit. Recorded steps, commit keys, and releases are isolated from application handlers.

## Under the hood

The `HotkeySequenceRecorderController` creates a `HotkeySequenceRecorder` class instance and subscribes to its reactive state via `@tanstack/store`. The class manages its own keyboard event listeners and state, and the controller handles cleanup on disconnect.
