Skip to main content

In · Out · Ripple

How a video editor timeline works: tracks, clips, trims and overlaps

Learn the data model behind a non-linear video timeline: tracks, clips, in and out points, trims, ripple edits and how overlapping clips are resolved.

15 min readEngineering

Part 4 of 15Build a browser video editor

In · Out · Rippleazelify
Engineering

How a video editor timeline works: tracks, clips, trims and overlaps

Key takeaways

  • A clip is a window onto a media file: where it sits on the timeline and which part of the source it plays are two separate ranges.
  • Something has to decide what draws on top; many editors use track order, while others keep paint order as a separate compositing setting.
  • Trim, ripple and roll edits are small rules about which neighbouring clips move, and writing them as pure functions makes them testable.

A video editor timeline is the row of blocks, running left to right, that tells a player when each piece of footage should appear. Every drag, trim and split you make on it changes a small set of numbers, and those numbers are what the program saves, undoes and finally renders into frames. This article builds that data model from the bottom up: what a clip is, why a clip needs two time ranges instead of one, how tracks decide what sits on top, and why most trim edits are really rules about which neighboring clips move.

We have built a timeline for our own browser editor, so the worked examples come from that experience. The goal is not to tour every feature. It is to show the shape of the data and the reasons behind it, so you can model a timeline in plain JavaScript and test it without a screen. If the wider idea is new to you, it belongs to a family called non-linear editing, which the first section defines. The complete architecture of a browser video editor shows where the timeline fits.

What is a timeline, and what is a clip?#

Start with three words. A timeline is a horizontal axis measured in seconds, where zero is the first frame of the project. A track is one lane along that axis; most editors show video tracks stacked above audio tracks. A clip is one use of one piece of media on one track. It says, in effect, "play this part of that file, starting here." The media file itself, which we call the source, is never changed by editing. The clip is only a pointer with a few numbers attached.

The term non-linear editing comes from the older method of copying tape from reel to reel in strict order. Wikipedia describes the newer approach as non-destructive: edits are stored as a list of pointers rather than applied to the original files, so the source stays intact and any edit can be changed later.[1] Random access is the other half of the idea. You can jump to any point in the sequence without rewinding the whole tape. Video editing, the broader craft of arranging shots after they are recorded, is outlined in Wikipedia's general article.[2]

In code, a clip can be a plain object. The shape below is a teaching version, not a production schema:

// A clip is a pointer: "play this window of that file, here on the timeline".
// All times are in seconds.
interface Clip {
  id: string;            // unique, never reused after a delete
  mediaId: string;       // which source file this clip plays
  trackId: string;       // which lane the clip sits on
  timelineStart: number; // where the clip begins on the timeline
  sourceIn: number;      // where playback starts inside the source
  sourceOut: number;     // where playback stops inside the source
}

// A 60-second source. This clip plays seconds 10 to 14 of it,
// starting at second 4 of the project.
const interview: Clip = {
  id: "clip-1",
  mediaId: "interview.mp4",
  trackId: "video-1",
  timelineStart: 4,
  sourceIn: 10,
  sourceOut: 14,
};

Notice what the object does not contain: no pixels, no decoded frames, and no copy of the file. Keeping the model this small is the first design decision. The timeline answers questions about time only. Drawing and decoding happen later, in other parts of the system.

Fig. 01
Clip A0–4s
Clip B4–12s
0s5s10s
4s · Cut
Two clips tile a 12-second track with a single cut at 4 secondsA horizontal timeline of 12 seconds on one track. Clip A runs from 0 to 4 seconds and reads its source from 2 to 6 seconds. Clip B runs from 4 to 12 seconds and reads its source from 10 to 18 seconds. A marker labeled Cut sits at 4 seconds, where the two clips meet with no gap and no overlap.12
  1. 1Clip A 0–4sin 2s · out 6s
  2. 2Clip B 4–12sin 10s · out 18s
Two clips tile a 12-second track with a single cut at 4 seconds
Illustrative, not data

The figure above shows a whole track rather than a single clip. Two clips tile it with no gap and no overlap, and the cut sits where the first clip ends. Each clip's detail text names its source range, so you can see that the timeline range and the source range are different numbers.

Why does a clip need two ranges, not one?#

Look at the interview clip again. It occupies the timeline from second 4 to second 8, which is four seconds long. It also reads the source from second 10 to second 14, which is also four seconds long. The two ranges have the same length but different positions. That is the whole reason a clip has two ranges: the timeline range says when something happens, and the source range says which part of the file happens then.

Once you accept that, trimming becomes simple to describe. If you trim the left edge of the clip by one second, the clip's timeline start moves right by one second, and the source in-point also moves right by one second. Both ranges move together, because the frame that was at the edge is now one second later in the file. If you moved the timeline start without moving the source in-point, you would stretch the content instead of cutting it.

Range boundaries are a small but important detail. We treat every range as half-open: it includes its start and excludes its end.[3] In shorthand, [4, 8) contains 4 and stops just before 8. This makes back-to-back clips safe. A clip ending at 8 and a clip starting at 8 do not overlap, because the first one never owns the instant 8.

The table below lists the fields a clip needs and where each one lives. Keep it beside you while you read the next sections.

FieldLives onUnitWhat it answers
timelineStartthe clipsecondsWhen does this clip begin in the project?
sourceInthe clipsecondsWhere in the file does playback begin?
sourceOutthe clipsecondsWhere in the file does playback end?
trackIdthe clipan idWhich lane is it on?
mediaIdthe clipan idWhich file supplies the pixels?

The same fields appear in an edit decision list, a plain-text record of cuts that editors use to hand a sequence from one program to another. An EDL lists each edit with its source in and out points and its timeline in and out points.[4] It is a useful reminder that the timeline is a list of decisions, not a list of media.[5] Positions inside footage are often written as timecode, a sequence of numeric codes generated at regular intervals.[6]

Many editors also store the duration as its own field. Deriving it from sourceOut - sourceIn means the two can never disagree, at the cost of computing it wherever you need it. Either choice works; what matters is that you pick one and stick to it.

How do tracks, stacking and overlaps decide what you see?#

Tracks give the timeline its second dimension. Along one track, clips run in order and do not share time. Across tracks you can stack things: a title over a video, or a music bed under dialogue. In most editors, track order decides what draws on top, with the higher track painting last. Stacking, overlap rules and transitions all start from that single ordering decision.

Overlap rules keep the sequence from becoming ambiguous. If two clips on the same track share a moment, the answer to "what should show at second 6?" is unclear. So most editors refuse that shape, with one deliberate exception. A transition is a declared overlap between two adjacent clips on the same track, such as a crossfade. The transition names the pair and records how much time they share. Anything else is an unowned overlap, and it should be rejected.

Here is the collision check as a pure function, using the half-open rule from the previous section:

// The timeline end of a clip, exclusive: the range is [timelineStart, end).
function clipEnd(c: Clip): number {
  return c.timelineStart + (c.sourceOut - c.sourceIn);
}

// Two clips collide when they share a track and their ranges intersect.
// Overlap on DIFFERENT tracks is fine: that is layering, not a conflict.
function collides(a: Clip, b: Clip): boolean {
  if (a.id === b.id || a.trackId !== b.trackId) return false;
  return a.timelineStart < clipEnd(b) && b.timelineStart < clipEnd(a);
}

Check it by hand. On one track, [4, 8) and [8, 12) do not collide, because 4 is less than 12 but 8 is not less than 8. The pair [4, 8) and [6, 10) does collide, because 4 is less than 10 and 6 is less than 8. Nothing in the function touches the screen, so you can test it with numbers alone.

The one thing you must not confuse is track order with paint order. Track order in a panel is an editing convenience: which lanes exist and how they are arranged. Which pixels win is a compositing decision, made when the frames are drawn. We keep the two apart on purpose, and the Azelify section below explains why.

Ripple, roll, slip and slide: which clips move?#

Trimming has four standard forms, and each name describes what is allowed to change. The table uses the definitions from a working editor's guide to the trim tool.[7]

EditWhat you dragThis clip's lengthNeighboring clipsTotal timeline length
Rippleone clip's edgechangeslater clips shift to followchanges
Rollthe cut between two clipsone grows, one shrinksnothing else movesstays the same
Slipthe footage inside a clipunchangedunaffectedstays the same
Slidethe clip itselfunchangedneighbors absorb the gapstays the same

Ripple is the only one that alters the length of the whole sequence. Roll moves a single cut point between two neighbors, so one clip gets longer while the other gets shorter. Slip changes which part of the source plays inside a fixed box. Slide moves a box along the track and lets its neighbors shrink or grow to fill the space. Each rule is a small question about which numbers change, and the decision tree below turns those questions into a sequence of yes-or-no answers.

Fig. 02
Choosing a trim edit by asking what has to stay fixedStart by asking whether total timeline length changes. If yes, the edit is a ripple. If no, ask whether the clip itself moves: if it does, it is a slide. If the clip stays put, ask whether the cut point moves: if it does, it is a roll, and if it does not, it is a slip.yesnoyesnoyesno
Does total length change?
Ripple: later clips shift
Does the clip itself move?
Slide: neighbors absorb it
Does the cut point move?
Roll: two clips trade time
Slip: content moves, not the box
  1. Does total length change?
    1. yes → Ripple: later clips shift
    2. no → Does the clip itself move?
      1. yes → Slide: neighbors absorb it
      2. no → Does the cut point move?
        1. yes → Roll: two clips trade time
        2. no → Slip: content moves, not the box
Choosing a trim edit by asking what has to stay fixed
Illustrative, not data[premiumbeat]

Every one of these edits is a rewrite of the same few fields: timelineStart, sourceIn and sourceOut, on one or two clips. A ripple also rewrites the timelineStart of every later clip on the track. Once you see trims as rewrites of numbers, you can implement them without any user interface at all, and that is what makes them testable.

Why write edits as pure functions?#

A pure function is one whose result depends only on its inputs, and which changes nothing else in the program. Wikipedia gives two defining properties: the return values are identical for identical arguments, and the function has no side effects.[8] For a timeline, that means a trim takes a clip and a number, and returns a new clip. It never modifies the clip it was given and never reaches into the undo history or the screen.

Here is a trim of the left edge written that way:

const MIN_CLIP_SECONDS = 0.2; // hard-coded constant: the shortest clip we allow

// Trim the left edge of a clip by `seconds`. Returns a NEW clip.
// The source window moves with the edge, so the content is cut, not stretched.
function trimStart(clip: Clip, seconds: number): Clip {
  const length = clip.sourceOut - clip.sourceIn;
  // Keep at least MIN_CLIP_SECONDS on the timeline.
  const amount = Math.min(Math.max(seconds, 0), length - MIN_CLIP_SECONDS);
  return {
    ...clip,
    timelineStart: clip.timelineStart + amount, // the box moves right
    sourceIn: clip.sourceIn + amount,           // the window moves with it
  };
}

A test for it is short, runs in Node, and states the rule exactly:

import assert from "node:assert/strict";
import { test } from "node:test";

test("trimStart moves the timeline start and the source in-point together", () => {
  const before: Clip = {
    id: "clip-1", mediaId: "m", trackId: "v1",
    timelineStart: 4, sourceIn: 10, sourceOut: 14,
  };
  const after = trimStart(before, 1);
  assert.equal(after.timelineStart, 5);
  assert.equal(after.sourceIn, 11);
  assert.equal(after.sourceOut, 14);     // the right edge did not move
  assert.equal(before.timelineStart, 4); // the input was never changed
});

The payoff is in the last assertion. Because the function never mutates its input, the editor can keep the old clip for undo, compare the two versions, or run the same edit twice and get the same answer. Mutable code can pass a happy-path test and still corrupt a history stack, so we treat purity as a design rule rather than a style preference. The companion post on undo and redo builds directly on this property.

From a drag gesture to a saved clip#

A user drags an edge with a mouse, a pen or a finger. Each of those inputs reaches the page as a pointer event (opens in a new tab),[9] so the gesture arrives as pixels, but the model stores seconds. The bridge between the two is the zoom level. If the timeline shows 100 pixels per second, a 10-pixel move is 0.1 seconds. If the user zooms in to 400 pixels per second, the same 10-pixel move is only 0.025 seconds. The conversion has to happen at the moment of the drag, because the zoom can change between gestures.

Snapping follows the same logic. A snap threshold is a distance in pixels within which a dragged edge jumps to a nearby edge, a playhead or a marker. Expressed in time, it is the pixel threshold divided by the current scale. That is why snapping feels the same at every zoom level: the pixel distance stays constant, and the time distance adapts to it.

// Snap a dragged time to the nearest candidate within a pixel threshold.
function snapTime(
  dragged: number,
  candidates: number[],   // edges and markers, in seconds
  pixelsPerSecond: number,
  snapPixels = 10,        // a ten-pixel snap distance, at any zoom
): number {
  const threshold = snapPixels / pixelsPerSecond; // pixels -> seconds
  let best = dragged;
  let bestDistance = threshold;
  for (const c of candidates) {
    const distance = Math.abs(c - dragged);
    if (distance <= bestDistance) {
      best = c;
      bestDistance = distance;
    }
  }
  return best;
}

The full path from a drag to a saved clip has four stages, which the flow diagram shows.

Fig. 03
A drag becomes a saved clip in four separate, testable stagesFour boxes in sequence. First, the gesture turns pixel movement into a candidate time. Second, snapping pulls that time to a nearby edge when it falls within the pixel threshold. Third, validation checks the overlap rules and refuses an illegal result. Fourth, an apply step writes the new clip record and records one undo entry.
Drag gesturepixels to seconds
Snapwithin N pixels
Validateno unowned overlap
Applyone undo entry
  1. 01 → Drag gesturepixels to seconds
  2. 02 → Snapwithin N pixels
  3. 03 → Validateno unowned overlap
  4. 04 → Applyone undo entry
A drag becomes a saved clip in four separate, testable stages
Illustrative, not data

Each stage is a separate function, so each one can be tested. The gesture produces a candidate time. Snapping adjusts it. Validation checks the overlap rules. Only then does the editor apply the change to the clip record. If validation fails, the clip record is left alone and the user sees a refusal rather than a silent change.

Step-by-step: build a minimal timeline model#

The four steps below take you from an empty file to a tested timeline model. Each step is small enough to finish in one sitting, and each one ends with a check you can run.

Step 1: Define the clip record#

Start with the Clip interface from the first section, and add nothing else yet. Resist the urge to store the file's width, a color or a thumbnail. Those belong to other stages. A clip record needs an id, a media id, a track id and three times. If you find yourself adding a fourth time field, check whether it is derivable from the three you have.

Step 2: Reject overlaps on the same track#

Add collides and a validator that checks every pair. The nested loop is quadratic, which is fine for a teaching example with a few dozen clips. A production editor sorts clips by start time within each track and checks only neighbors, but the rule is identical.

// Returns one message per collision. An empty array means the timeline is valid.
function validateNoOverlap(clips: Clip[]): string[] {
  const errors: string[] = [];
  for (let i = 0; i < clips.length; i++) {
    for (let j = i + 1; j < clips.length; j++) {
      if (collides(clips[i], clips[j])) {
        errors.push(`${clips[i].id} overlaps ${clips[j].id} on ${clips[i].trackId}`);
      }
    }
  }
  return errors;
}

Then write one test with two clips that touch at 8, and one pair that overlaps by a second. The touching pair must pass and the overlapping pair must fail. That test pins the half-open rule in place.

Step 3: Write one edit as a pure function#

Pick the simplest edit first: trimStart from the earlier section. Keep it pure, give it a name that says what it does, and have it return a new clip. Do not connect it to a mouse event yet. A function that only maps numbers is far easier to get right than one that also reads the cursor.

Step 4: Test the boundary cases#

Write one test per edge: a zero-length trim, a trim larger than the clip, and a trim that would make the clip shorter than the minimum. Then add one property you always want to hold, such as "a left trim never moves the right edge." Only when those pass should you connect the function to a gesture. The gesture then becomes a thin layer over logic you have already tested.

How the Azelify editor handles the timeline#

The Azelify editor follows the same shape, with a few decisions that are worth describing as trade-offs. The first is that the timeline owns no storage of its own. Tracks, clips, nested sequences and multicam groups are all fields on the project document, and the timeline code is pure logic over that shape. Keeping storage in one place gives the rest of the system one thing to read. The cost is that every timeline operation has to be expressed through the document rather than through private state.

The second decision is that every change goes through the action layer that records undo. The timeline code has no direct write path of its own. That sounds like plumbing, but it is what makes "undo the last trim" a dependable operation. The trade-off is ceremony: a new kind of edit needs an action definition before it can touch a clip.

The third decision is the overlap rule. Only a declared transition can share time on a track. If an edit would create an unowned overlap, the action validator refuses it, the document is unchanged, and the user sees one consistent message. The benefit is that the renderer never has to guess which of two clips should win. The cost is that the editor sometimes says no where a looser tool would quietly stack the clips.

The fourth decision concerns nested sequences, which the editor calls compound clips. The nested content is stored once, beside the timeline, and the outer timeline holds an ordinary clip that points to it. Two instances can share one definition, so an edit inside reaches both. When you want one instance to change without the other, the editor makes an independent copy. Sharing is efficient, and copying is an explicit choice.

Two smaller choices round out the model. A group is a selection convenience with no enforced propagation. A link is an editing constraint: moving one linked clip moves its partner, which keeps a J-cut's video and its detached audio together. And a three-point edit, which copies a span of source into a span of timeline, derives the fourth of its four numbers from the other three. The solver does not check whether a caller's fourth value agrees with the rest. It overwrites that value. This is a deliberate simplification, and it is worth knowing about if you build the same feature.

Finally, scene splitting does not run a new detection pass inside the editor. It reads cut points that were calculated when the media was imported. That keeps the timeline fast and keeps the analysis in the place where the file is first handled. The next post in the series covers importing video files in the browser, where that analysis begins.

Frequently asked questions#

Is a clip the same thing as a media file?#

No. A media file is the source footage on disk or in storage, and editing never changes it. A clip is a small record that says which window of that file plays, on which track, and starting at which time. Many clips can point at the same file, and deleting a clip does not delete the file.

Why store times in seconds instead of frames?#

Seconds are easy to read and easy to scale with zoom. Frame counts are exact for one frame rate, and rates differ widely, from 50 or 60 frames per second in analog television to 120 or more in newer standards.[10] Decimal seconds can accumulate rounding error, so many editors snap values to a frame grid when an edit is committed. Pick one unit, and convert only at the boundary where you draw or export.

Do I need React to build a timeline?#

No. The data model in this article is plain TypeScript with no framework. A React component is only one way to draw it. Keeping the model separate means you can test trims in Node, reuse the same logic in a worker, and swap the view later without rewriting the rules.

Can two clips play at the same time on one track?#

Normally no. An overlap on one track is refused unless it is a declared transition, such as a crossfade between two neighbors. Clips on different tracks can overlap freely, and that is how titles and music sit on top of footage.

What does ripple mean in a video editor?#

A ripple edit changes one clip's length and pushes every later clip along the track by the same amount. It is the only trim that changes the total length of the sequence. Use it when you want to remove a gap or tighten a scene without leaving a hole.

How do I undo a trim without copying the whole project?#

Store the before and after values of the clip, or store the command that produced the change, and reverse it when asked. Because trims are pure functions, the old clip is still available whenever you need it. The undo post explains the command pattern in detail.

Where to go next#

Read how undo and redo work in a command-based editor to see how the pure edits above become a reversible history. For the playback side, frame-accurate playback in JavaScript explains how the timeline's time becomes the exact frame you see.

Sources

  1. Wikipedia, “Non-linear editing system,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  2. Wikipedia, “Video editing,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  3. Wikipedia, “Interval (mathematics),” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  4. HowToFilmSchool, “Edit Decision List (EDL),” howtofilmschool.com (opens in a new tab) (accessed 2026-10-13). ↩

  5. Wikipedia, “Edit decision list,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  6. Wikipedia, “Timecode,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  7. PremiumBeat, “How to Use the Dynamic Trim Tool in Resolve,” www.premiumbeat.com (opens in a new tab) (accessed 2026-10-13). ↩

  8. Wikipedia, “Pure function,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

  9. MDN Web Docs, “Pointer events,” developer.mozilla.org (opens in a new tab) (accessed 2026-10-13). ↩

  10. Wikipedia, “Frame rate,” en.wikipedia.org (opens in a new tab) (accessed 2026-10-13). ↩

Build a browser video editor

In this series

  1. 01How to build a video editor in the browser: the complete architecture
  2. 02Designing the project file for a web video editor: schema, versions and autosave
  3. 03Undo and redo in a web editor: the command pattern, explained from zero
  4. 04How a video editor timeline works: tracks, clips, trims and overlaps
  5. 05Importing video files in the browser: probing, codecs, proxies and storage
  6. 06WebCodecs tutorial: decoding video frames in the browser without a video tag
  7. 07Frame-accurate playback in JavaScript: clocks, requestAnimationFrame and A/V sync
  8. 08Compositing video layers with Canvas and WebGL: transforms, blends and effects
  9. 09Text and animated captions in a browser video editor: let CSS do the drawing
  10. 10Drag, resize and rotate handles for a canvas editor: building the transform gizmo
  11. 11Web Audio API for video editors: mixing tracks, drawing waveforms and loudness
  12. 12Exporting MP4 from the browser: WebCodecs encoding, muxing and server fallback
  13. 13Keyframe animation in a web editor: interpolation, easing curves and motion tracking
  14. 14How to test a video renderer: golden images, oracles and property-based tests
  15. 15FFmpeg.wasm vs WebCodecs vs MediaRecorder: choosing a browser video stack

Cut a timeline in the browser

Drop in footage and trim, split and stack clips on real tracks.

Open the studio

Keep reading

More in Browser video editor

All guides