# Timeline

The timeline is the row under the video with one segment per scene, each as long as its scene plays. It is how a viewer scrubs, and with `editing` on it is also the whole editor: hover a segment to switch that scene off, move it earlier or later, or open it for editing. There is no separate scene list to keep in sync.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/player/timeline/
>
> Hover a segment (tap it on a phone): arrows, a pencil and a switch.

## Editing from the player

`WrappedPlayer` draws the timeline for you. Pass it `editing` and the hover card of every segment gets its controls. The player never changes the cut itself: each control calls you, you change the config, and the scenes you pass back are the new video.

`editable-cut.tsx`

```tsx
import { type CSSProperties, useMemo, useState } from "react";

import { arrangeScenes, type SceneInstance, type WrappedConfig, type WrappedSubject } from "@react-wrapped/core";
import { WrappedPlayer } from "@react-wrapped/player";

const ROOM: CSSProperties = { display: "grid", height: 560 };

export function EditableCut({
  subject,
  seed,
  scenes,
  config,
  onConfigChange,
}: {
  subject: WrappedSubject;
  seed: string;
  scenes: readonly SceneInstance[];
  config: WrappedConfig;
  onConfigChange: (next: WrappedConfig) => void;
}) {
  const [asked, setAsked] = useState<string | null>(null);
  // Every scene in the viewer's order, the switched-off ones too: the timeline shows them all.
  const ordered = useMemo(() => arrangeScenes(scenes, config.order), [scenes, config.order]);
  // Only what is still on gets played. Memoized: the player rebuilds its timeline when this array changes.
  const playing = useMemo(
    () => ordered.filter((scene) => !config.hidden.includes(scene.key)),
    [ordered, config.hidden],
  );

  const keys = ordered.map((scene) => scene.key);
  const editing = {
    scenes: ordered,
    hidden: config.hidden,
    onToggle: (key: string, on: boolean) =>
      onConfigChange({
        ...config,
        hidden: on ? config.hidden.filter((hidden) => hidden !== key) : [...config.hidden, key],
      }),
    onMove: (key: string, by: -1 | 1) => {
      const from = keys.indexOf(key);
      const to = from + by;
      if (from === -1 || to < 0 || to >= keys.length) return;
      const order = [...keys];
      [order[from], order[to]] = [order[to], order[from]];
      onConfigChange({ ...config, order });
    },
    // The pencil. Open your own editor here, or the library's `SceneEditor`.
    onEdit: setAsked,
  };

  return (
    <div>
      <div style={ROOM}>
        <WrappedPlayer subject={subject} seed={seed} scenes={playing} config={config} editing={editing} />
      </div>
      <output>
        {`hidden: [${config.hidden.join(", ")}] order: [${config.order.join(", ")}]`}
        {asked ? ` asked to edit: ${asked}` : ""}
      </output>
    </div>
  );
}
```

| Key | Type | What it does |
| --- | --- | --- |
| `scenes` | `readonly SceneInstance[]` | Every scene in the viewer's order, the switched-off ones too. Only on the player's `editing`: the timeline takes it as its own `scenes`. |
| `hidden` | `readonly string[]` | The keys of the scenes that are switched off. |
| `onToggle` | `(key: string, on: boolean) => void` | The switch in the hover card was flipped. |
| `onMove` | `(key: string, by: -1 \| 1) => void` | An arrow was pressed: one place earlier, or one place later. |
| `onEdit` | `(key: string) => void` | The pencil was pressed. Open an editor for that scene. |

## What the viewer gets

- **Scrub.** Click or drag anywhere on the row. The video pauses on that moment.
- **A still.** Hovering a segment shows the finished scene, its name and how long it plays. On touch a tap opens the same card.
- **The switch.** A scene switched off shrinks to a hollow sliver and stops playing. It stays on the row, so it can be switched back on. The last scene still on cannot be switched off.
- **The arrows** move a scene one place earlier or later.
- **The pencil** is there when the scene has texts to rewrite or a transition to pick.
- **Rarity.** A scene placed with `drop` wears its rarity colour, and glows once when it joins the row, so a find is easy to spot.

## Keeping new scenes in place

`arrangeScenes(scenes, order)` from `@react-wrapped/core` puts scenes in the viewer's order. A scene the viewer has never placed, such as one the [scan](https://bettertyped.github.io/react-wrapped/docs/scanner/) just found, goes right after the scene you put before it, so a find lands in its story slot even after the cut was reordered by hand. Your last scene stays last.

## Without the player

`Timeline` is exported for a transport of your own. It needs what `useWrappedTimeline` returns: the marks, the duration and a seek function, plus a ref the engine fills the segments through. This is the whole of a small player with its own play button:

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/player/timeline/

`own-transport.tsx`

```tsx
import { type CSSProperties, useRef } from "react";

import {
  type SceneInstance,
  useWrappedTimeline,
  WRAPPED_FORMATS,
  type WrappedConfig,
  WrappedStage,
  type WrappedSubject,
} from "@react-wrapped/core";
import { Timeline } from "@react-wrapped/player";

/** How wide the frame is drawn on the page. The stage itself is always full size and scaled to this. */
const WIDTH = 280;

export function OwnTransport({
  subject,
  seed,
  scenes,
  config,
}: {
  subject: WrappedSubject;
  seed: string;
  scenes: readonly SceneInstance[];
  config: WrappedConfig;
}) {
  const stageRef = useRef<HTMLDivElement>(null);
  const trackRef = useRef<HTMLDivElement>(null);
  const clockRef = useRef<HTMLSpanElement>(null);
  // The engine behind `WrappedPlayer`: it builds the timeline and writes the clock and the segments through the refs.
  const { playing, marks, toggle, seekTime, duration } = useWrappedTimeline({
    stageRef,
    scenes,
    config,
    seed,
    trackRef,
    clockRef,
  });

  const format = WRAPPED_FORMATS[config.format];
  const scale = WIDTH / format.width;
  const frame: CSSProperties = {
    position: "relative",
    width: WIDTH,
    height: format.height * scale,
    overflow: "hidden",
    borderRadius: 16,
  };

  return (
    <div>
      <div style={frame}>
        <WrappedStage stageRef={stageRef} subject={subject} scenes={scenes} config={config} scale={scale} />
      </div>
      <div role="group" aria-label="Transport">
        <button type="button" onClick={toggle}>
          {playing ? "Pause" : "Play"}
        </button>
        <Timeline
          trackRef={trackRef}
          scenes={scenes}
          marks={marks}
          duration={duration}
          config={config}
          onSeek={seekTime}
        />
        <span ref={clockRef}>0:00 / 0:00</span>
      </div>
    </div>
  );
}
```

## Props

| Prop | Type | What it does |
| --- | --- | --- |
| `trackRef` | `RefObject<HTMLDivElement \| null>` | A ref for the row of segments. The engine fills each segment's bar through it as the video plays. |
| `scenes` | `readonly SceneInstance[]` | Every scene in order. With `editing`, that includes the ones switched off. |
| `marks` | `readonly SceneMark[]` | Where each playing scene starts. From `useWrappedTimeline`. |
| `duration` | `() => number` | The length of the video in seconds. From `useWrappedTimeline`. |
| `config` | `WrappedConfig` | For the format of the hover still, the speed and the rarity colours. |
| `editing?` | `TimelineEditing` | The creator's controls in the hover card. Leave it out for a plain scrubber. |
| `onSeek` | `(seconds: number) => void` | Called while the viewer clicks or drags along the row. |
| `className?` | `string` | A class for the timeline's root. |
| `classNames?` | `SlotClassNames<TimelineSlot>` | A class per part, by slot name: `"timeline-segment-bar"`, `"timeline-preview"`. See [styling](https://bettertyped.github.io/react-wrapped/docs/guides/styling/). |

## Related

- [SceneEditor](https://bettertyped.github.io/react-wrapped/docs/player/scene-editor/)
- [WrappedPlayer](https://bettertyped.github.io/react-wrapped/docs/player/wrapped-player/)
- [The config in the URL](https://bettertyped.github.io/react-wrapped/docs/guides/config-in-the-url/)
- [Scanner](https://bettertyped.github.io/react-wrapped/docs/scanner/)
- [Player](https://bettertyped.github.io/react-wrapped/docs/player/)

Source: https://bettertyped.github.io/react-wrapped/docs/player/timeline/
