Skip to content
react-wrapped

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.

hidden: [] order: []

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
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>
  );
}
KeyTypeWhat it does
scenesreadonly 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.
hiddenreadonly string[]The keys of the scenes that are switched off.
onToggle(key: string, on: boolean) => voidThe switch in the hover card was flipped.
onMove(key: string, by: -1 | 1) => voidAn arrow was pressed: one place earlier, or one place later.
onEdit(key: string) => voidThe 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 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:

My yearon Orbit.

@ada, Oct 2025 to Oct 2026

In the last 12 months you made

0

contributions.

OctNovDecJanFebMarAprMayJunJulAugSepOct

That's 4.6 a day, on 257 active days out of 365.

Second Wind

Epic

This year against last. The ring is where last year ended, the dot is where this one did.

Your week

You ship onWednesdays.

M
T
W
T
F
S
S

Weekends took 5% of your work.

AD

@ada

Orbit Wrapped, Oct 2025 to Oct 2026

Contributions1,687
Longest streak55 days
Biggest day53
Top languageTypeScript
The MarathonerEpic
Comet DragonLegendary
AD
@adaOrbit Wrapped
0:00 / 0:00
own-transport.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

PropTypeWhat it does
trackRefRefObject<HTMLDivElement | null>A ref for the row of segments. The engine fills each segment's bar through it as the video plays.
scenesreadonly SceneInstance[]Every scene in order. With editing, that includes the ones switched off.
marksreadonly SceneMark[]Where each playing scene starts. From useWrappedTimeline.
duration() => numberThe length of the video in seconds. From useWrappedTimeline.
configWrappedConfigFor the format of the hover still, the speed and the rarity colours.
editing?TimelineEditingThe creator's controls in the hover card. Leave it out for a plain scrubber.
onSeek(seconds: number) => voidCalled while the viewer clicks or drags along the row.
className?stringA class for the timeline's root.
classNames?SlotClassNames<TimelineSlot>A class per part, by slot name: "timeline-segment-bar", "timeline-preview". See styling.

Last updated . Read this page as Markdown.