How to Manage Gaps in Multi-Track Timelines with Ripple Edit Operations in Clypra
Clypra treats gaps as first-class timeline entities that automatically shift surrounding clips when ripple edit mode is enabled, using pure functions in gapEngine.ts to calculate affected clips while respecting protected gaps.
Clypra is an open-source video editing framework that reimagines timeline management by treating gaps as manipulable entities rather than passive empty space. When you need to manage gaps in multi-track timelines with ripple edit operations in Clypra, the engine provides precise control over temporal shifting while preventing collisions with protected sections. This architecture separates gap logic from state management, making ripple behavior predictable and testable across complex multi-track projects.
Understanding Gaps as First-Class Timeline Entities
Unlike traditional video editors that treat empty space as background, Clypra's architecture defines gaps as discrete objects with metadata. In src/lib/timeline/gapEngine.ts, gaps carry properties like protected: true or type "protected", allowing the ripple engine to distinguish between collapsible space and fixed boundaries. This design enables sophisticated timeline operations where gaps can be inserted, resized, or removed programmatically while maintaining sync relationships with other tracks.
Core Ripple Edit Operations in Clypra
The gap engine exposes four primary operations for managing timeline gaps. Each returns affectedClipIds—an array of clips that must shift to accommodate the temporal change.
Inserting Gaps with Ripple Effects
The insertGapWithRipple function validates duration and start time before creating a Gap object. According to the implementation in src/lib/timeline/gapEngine.ts, it identifies all clips on the same track whose startTime is greater than or equal to the insertion point and less than the next protected gap.
import { insertGapWithRipple } from '@/lib/timeline/gapEngine';
import { useTimelineStore } from '@/store/timelineStore';
const { clips, gaps, trackId } = useTimelineStore.getState();
const result = insertGapWithRipple(
trackId, // track where the gap will live
12.0, // start time (seconds)
3.0, // duration (seconds)
clips,
gaps,
);
// Shift the returned clips right by the gap duration
if (result.success) {
result.affectedClipIds.forEach((id) => {
// UI layer applies the temporal shift
});
}
Removing Gaps and Shifting Clips Left
When deleting space, removeGapWithRipple locates the nearest protected gap before the removed gap to establish a boundary. It selects only those clips that start after the gap and will not cross the protected-gap boundary when shifted left.
import { removeGapWithRipple } from '@/lib/timeline/gapEngine';
import { useTimelineStore } from '@/store/timelineStore';
const { clips, gaps } = useTimelineStore.getState();
const gapToDelete = gaps.find(g => g.id === 'gap-123');
if (gapToDelete) {
const { affectedClipIds } = removeGapWithRipple(gapToDelete, clips, gaps);
// Shift affected clips left by gapToDelete.duration
}
Resizing Gaps While Respecting Protected Boundaries
The resizeGap function computes deltaTime (new duration minus old duration). If the gap grows, shifting stops at the next protected gap; if it shrinks, following clips shift left safely without boundary violations.
import { resizeGap } from '@/lib/timeline/gapEngine';
const resized = resizeGap(gapToResize, 5.0, clips, gaps);
if (resized.success) {
// Apply new duration and shift affected clips
}
Packing Tracks to Collapse All Unprotected Gaps
The packTrack function removes all unprotected gaps and returns every clip on the track as affectedClipIds, allowing the UI to collapse them tightly against each other. This operation replaces gaps entirely rather than rippling them, making it ideal for final timeline compaction.
Enabling and Controlling Ripple Edit Mode
Ripple edit mode is toggled via the global timeline store at src/store/timelineStore.ts. The boolean rippleEditEnabled gates the ripple-aware trimming logic (rippleTrimClip) and is exposed through the shortcut store (src/store/shortcutStore.ts).
When disabled, trimming affects only the selected clip. When enabled, trim operations propagate time differences to the affectedClipIds returned by the gap engine, creating the classic "ripple" effect that maintains gaps between clips.
import { useTimelineStore } from '@/store/timelineStore';
const toggleRipple = () => {
useTimelineStore.setState(s => ({
rippleEditEnabled: !s.rippleEditEnabled,
}));
};
Protected Gaps and Timeline Safety
Protected gaps serve as immovable anchors in the timeline. All gap-engine functions check for protected: true before calculating shift distances. This ensures that ripple operations cannot overwrite locked sections, such as synchronized audio tracks or locked reference markers, even when performing aggressive ripple edits across multiple tracks.
Architecture: Separating Logic from State
Clypra's architecture enforces a clean separation between pure gap logic and state management. The gapEngine.ts file contains pure functions with no side effects, while timelineStore.ts (using Zustand) manages the mutable timeline state. This design makes ripple behavior testable, reusable across multiple tracks, and extensible for features like snap-to-gap functionality.
Summary
- Gaps as entities: Clypra treats gaps as first-class objects with metadata, not empty space.
- Four core operations: Insert, remove, resize, and pack gaps using
insertGapWithRipple,removeGapWithRipple,resizeGap, andpackTrackinsrc/lib/timeline/gapEngine.ts. - Protected boundaries: Gaps marked with
protected: trueprevent ripple operations from shifting clips beyond safe boundaries. - State separation: Pure functions in
gapEngine.tscalculate effects whiletimelineStore.tsmanages therippleEditEnabledflag and applies shifts. - Multi-track support: The engine returns
affectedClipIdsfor the UI to animate, supporting simultaneous operations across multiple tracks.
Frequently Asked Questions
What is the difference between ripple edit mode and standard trimming in Clypra?
Standard trimming modifies only the selected clip's duration or position. When ripple edit mode is enabled via rippleEditEnabled in timelineStore.ts, trimming operations propagate time changes to all subsequent clips on the track, automatically closing gaps or shifting content to maintain synchronization according to the gap engine's calculations.
How does Clypra prevent ripple edits from affecting locked timeline sections?
Clypra implements protected gaps—gaps flagged with protected: true or type "protected". Functions like insertGapWithRipple and removeGapWithRipple search for the nearest protected gap before calculating shift distances, ensuring clips never cross these boundaries during ripple operations.
Can I apply ripple edits to multiple tracks simultaneously?
While gapEngine.ts functions operate on individual tracks, the returned affectedClipIds can be processed across multiple tracks by the UI layer. The timelineStore.ts manages the global ripple state, allowing simultaneous applications of gap operations to selected tracks or the entire timeline.
Where is the ripple edit toggle stored and how is it accessed?
The toggle resides in src/store/timelineStore.ts as the boolean rippleEditEnabled. The shortcut store (src/store/shortcutStore.ts) registers the "Toggle Ripple Edit" action under ID "toggle-ripple-edit", which updates this flag and typically triggers UI feedback via hooks like useKeyboardShortcuts.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →