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:

  1. Clamp to video bounds using clampSpanToBounds to ensure regions do not exceed total video duration
  2. Prevent overlap by checking hasOverlap against allRegionSpans; if overlap exists, clampToNeighbours automatically adjusts the span to the nearest free space
  3. Enforce minimum duration via minItemDurationMs to prevent regions from becoming invisible
  4. Update application state through onItemSpanChange to 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.tsx instantiates the dnd-timeline context at lines 96-104 and orchestrates drag, resize, and range change callbacks.
  • Row.tsx registers track containers using the useRow hook (lines 13-20).
  • Item.tsx enables draggable region behavior via the useItem hook (lines 51-56), storing span and row identification data.
  • Constraint enforcement prevents region overlap through hasOverlap and clampToNeighbours, clamps spans to video bounds, and respects minimum durations.
  • Range management ensures the visible timeline window stays within valid video limits using clampRange and handleRangeChange.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →