# VideoAnnotator

A video player with an **editable, contiguous segment timeline** — the labeling
surface for splitting a clip into consecutive phases and captioning each one.
It owns the `<video>` element, a transport bar (frame-step, boundary jump,
play/pause, playback speed) and a percentage-positioned timeline strip with
drag-to-move boundaries and click-to-scrub. Segments always tile the whole clip
with no gaps or overlaps (`end === next.start`); any structural edit resets the
`verified` flag on the affected segments.

The component is **controlled** on `segments` + `selectedIndex`: it runs the
split/merge/boundary invariants internally and hands the host a fresh array via
`onSegmentsChange`, so the host owns state and persistence.

## Demo

Drag a boundary to move it, double-click a boundary to merge, click a segment to
select it, drag anywhere on the track to scrub. Keyboard: `Space` play/pause,
`←`/`→` step frame (`Shift` = 1s, `Alt` = jump boundary), `,`/`.` prev/next
boundary, `s` split, `Backspace` merge, `j`/`k` prev/next phase, `a` approve.

## Driving it from a ref

Set `enableKeyboard={false}` and drive the widget through its imperative handle
(`split`, `merge`, `stepFrame`, `gotoBoundary`, `play`, `pause`, `toggleApprove`)
when the host owns its own transport chrome or a global shortcut scheme.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `videoUrl` | `string \| null` | — | Video source URL. Empty shows a placeholder in the stage. |
| `videoTitle` | `string` | — | Title shown in a header row above the video (e.g. the clip name). |
| `videoSubtitle` | `string` | — | Monospace subtitle beside the title (e.g. the file path). |
| `headerLeading` | `ReactNode` | — | Element at the start of the header row (e.g. a "show list" button). |
| `showDescription` | `boolean` | `false` | Render the boxed description editor for the selected segment below the timeline. |
| `onDescriptionChange` | `(index: number, value: string) => void` | — | Fired when the description editor changes. Required for it to be interactive. |
| `segments` | `Segment[]` | — | Controlled segment list. The component enforces the contiguous invariant. |
| `selectedIndex` | `number` | — | Controlled index of the active segment. |
| `onSegmentsChange` | `(next: Segment[]) => void` | — | Fired after any structural edit (split / merge / boundary drag). |
| `onSelectedChange` | `(index: number) => void` | — | Fired when the active segment changes. |
| `onApproveToggle` | `(index: number, verified: boolean) => void` | — | Fired when the user toggles verification (`a`). |
| `duration` | `number` | — | Authoritative clip duration (s). Falls back to the video's `loadedmetadata` duration, then the largest segment end. |
| `extractFps` | `number \| null` | `30` | Frames-per-second for frame-stepping granularity (`←`/`→`). |
| `srcFps` | `number \| null` | `30` | Source fps for the "· fN" frame readout. |
| `loop` | `boolean` | `true` | Loop playback within the selected segment. |
| `speeds` | `number[]` | `[0.25,0.5,1,1.5,2]` | Playback-speed options for the speed dropdown. |
| `enableKeyboard` | `boolean` | `true` | Install the document-level keyboard shortcuts. Set `false` to drive only via the ref. |
| `className` | `string` | — | Extra classes on the root element. |

### Segment

| Field | Type | Description |
| --- | --- | --- |
| `start` | `number` | Segment start time in seconds. |
| `end` | `number` | Segment end time in seconds (`= next.start` after normalization). |
| `description` | `string` | Free-text caption for the phase. |
| `verified` | `boolean` | Human-confirmed flag; reset by structural edits. |
