How OpenScreen Integrates dnd-timeline for Drag-and-Drop Region Management
OpenScreen wraps the dnd-timeline library in a custom TimelineWrapper component that supplies global timeline state, delegates drag-and-drop callbacks, and enforces video-specific constraints like overlap prevention and duration clamping.
OpenScreen is an open-source video editor built by siddharthvaddem/openscreen that leverages dnd-timeline to power its track-based editing interface. The integration centers on a three-layer React architecture—Context, Row, and Item—that bridges generic timeline primitives with domain-specific requirements for managing zoom, trim, speed, and annotation regions.
Three-Layer Architecture for Timeline Integration
The integration strategy separates concerns across three distinct layers, each responsible for registering entities with the underlying dnd-timeline engine.
Context Wrapper in TimelineWrapper.tsx
The foundation is established in src/components/video-editor/timeline/TimelineWrapper.tsx, where the <TimelineContext> from dnd-timeline is instantiated at lines 96-104. This provider forwards the current video range, handles range changes, and wires custom drag and resize end logic through callbacks including onDragStart, onDragMove, onDragEnd, onResizeMove, and onResizeEnd.
Row Registration via useRow
Each track (video, audio, or annotation) is rendered using Row.tsx, which calls the useRow hook at lines 13-20. This hook registers the row with the timeline context and returns a ref and style objects necessary for layout calculations.
Item Definition via useItem
Draggable and resizable regions are created in Item.tsx through the useItem hook (lines 51-56). This hook provides drag listeners, attributes, and styling while storing each item's span (start/end milliseconds) and custom rowId data required for overlap detection.
Implementing Drag and Resize Callbacks
OpenScreen implements a complete lifecycle for drag-and-resize interactions within TimelineWrapper.tsx, intercepting dnd-timeline events to inject video-specific logic.
Initiating Interactions
When a user begins dragging or resizing, the onDragStart callback (lines 21-23) extracts the current span via the item's data callbacks and triggers showTooltip to display a time indicator under the cursor.
Real-time Position Updates
During movement, onDragMove (lines 28-35) and onResizeMove (lines 39-46) keep the tooltip positioned under the pointer using the delta values supplied by dnd-timeline. These callbacks update the UI without committing changes to state.
Finalizing Region Changes
The validation and persistence logic resides in onDragEnd (lines 65-84) and onResizeEnd (lines 30-53). These handlers perform four critical operations:
- Clamp to video bounds using
clampSpanToBoundsto ensure regions do not exceed total video duration - Prevent overlap by checking
hasOverlapagainstallRegionSpans; if overlap exists,clampToNeighboursautomatically adjusts the span to the nearest free space - Enforce minimum duration via
minItemDurationMsto prevent regions from becoming invisible - Update application state through
onItemSpanChangeto persist the validated span
These callbacks are passed back to <TimelineContext> so the underlying dnd-timeline engine invokes them at the appropriate moments.
Enforcing Video Constraints in the Timeline
While dnd-timeline provides generic span manipulation, OpenScreen must enforce constraints specific to video editing. The handleRangeChange function (lines 69-92) normalizes the visible time range using clampRange, ensuring it never exceeds the total video duration (totalMs) and respects minVisibleRangeMs to maintain a usable zoom level.
Because dnd-timeline has no intrinsic knowledge of video boundaries or region semantics, OpenScreen implements custom validation logic that prevents regions from extending beyond media bounds, enforces non-overlapping track constraints, and provides real-time tooltip feedback displaying precise "mm:ss.s – mm:ss.s" timestamps during drag operations.
Code Implementation Examples
Minimal Timeline Setup
import TimelineWrapper from "@/components/video-editor/timeline/TimelineWrapper";
import Row from "@/components/video-editor/timeline/Row";
import Item from "@/components/video-editor/timeline/Item";
function VideoEditor({ videoDuration }) {
const [range, setRange] = useState<Range>({ start: 0, end: 5000 });
const [items, setItems] = useState([
{ id: "zoom1", rowId: "video", span: { start: 0, end: 2000 }, variant: "zoom" },
// …more items
]);
const hasOverlap = (span, excludeId) => {
return items.some(i => i.id !== excludeId && spansOverlap(i.span, span));
};
const onItemSpanChange = (id, newSpan) => {
setItems(prev => prev.map(i => (i.id === id ? { ...i, span: newSpan } : i)));
};
return (
<TimelineWrapper
range={range}
videoDuration={videoDuration}
hasOverlap={hasOverlap}
onRangeChange={setRange}
minItemDurationMs={100}
minVisibleRangeMs={500}
onItemSpanChange={onItemSpanChange}
>
<Row id="video" label="Video">
{items
.filter(i => i.rowId === "video")
.map(i => (
<Item key={i.id} {...i} />
))}
</Row>
{/* additional rows for audio, annotations, etc. */}
</TimelineWrapper>
);
}
Custom Overlap Detection
function spansOverlap(a: Span, b: Span) {
return a.start < b.end && b.start < a.end;
}
Pass this function to TimelineWrapper via the hasOverlap prop to enable the built-in clampToNeighbours logic that resolves conflicts by sliding regions to the nearest available space.
Tooltip Feedback Implementation
<div
ref={tooltipRef}
className="absolute top-1 pointer-events-none z-[60] px-1.5 py-0.5 rounded bg-black/80 text-[10px] text-white/90 font-medium tabular-nums whitespace-nowrap border border-white/10 shadow-lg"
style={{ opacity: 0, transition: "opacity 0.1s" }}
/>
The wrapper updates this element's position and text content during active drag or resize operations to provide real-time temporal feedback.
Summary
TimelineWrapper.tsxinstantiates the dnd-timeline context at lines 96-104 and orchestrates drag, resize, and range change callbacks.Row.tsxregisters track containers using theuseRowhook (lines 13-20).Item.tsxenables draggable region behavior via theuseItemhook (lines 51-56), storing span and row identification data.- Constraint enforcement prevents region overlap through
hasOverlapandclampToNeighbours, clamps spans to video bounds, and respects minimum durations. - Range management ensures the visible timeline window stays within valid video limits using
clampRangeandhandleRangeChange.
Frequently Asked Questions
What is dnd-timeline and why does OpenScreen use it?
dnd-timeline is a React library that provides primitives for building interactive timeline interfaces with drag-and-drop capabilities. OpenScreen uses it to handle the complex geometry calculations, pointer event handling, and collision detection required for track-based video editing, while layering custom logic for video-specific constraints on top.
How does OpenScreen prevent regions from overlapping during drag operations?
When onDragEnd or onResizeEnd fires in TimelineWrapper.tsx (lines 30-53 and 65-84), the code checks for overlaps using the hasOverlap function against allRegionSpans. If an overlap is detected, clampToNeighbours automatically adjusts the region's start or end time to the nearest available gap rather than rejecting the drag operation entirely.
Where is the timeline context initialized in the codebase?
The dnd-timeline context is initialized in src/components/video-editor/timeline/TimelineWrapper.tsx at lines 96-104, where the <TimelineContext> component is rendered with callbacks for onDragStart, onDragMove, onDragEnd, onResizeMove, and onResizeEnd.
What constraints does OpenScreen enforce beyond dnd-timeline's defaults?
OpenScreen implements four key constraints: regions cannot extend beyond the total video duration (totalMs), regions must maintain a minimum visible duration (minItemDurationMs), regions cannot overlap on the same track (hasOverlap/clampToNeighbours), and the visible range must stay within sane zoom limits (minVisibleRangeMs).
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 →