**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  



> 📚 **Full documentation:** [github.com/pip-install-python/dash-nle-timeline](https://github.com/pip-install-python/dash-nle-timeline) — complete API reference and examples in the repository. This page is the quick-start overview.

### Installation

[Visit GitHub Repo](https://github.com/pip-install-python/dash-nle-timeline) · [PyPI](https://pypi.org/project/dash-nle-timeline/)

```bash
pip install dash-nle-timeline
```

### Introduction

A calendar or scheduler widget thinks in *dates*. A video editor needs an integer **frame** grid, a real draggable playhead, snapping to clip edges / markers / grid, audio waveforms, and edits expressed as data. `dash-nle-timeline` is that — a purpose-built **non-linear-editor (NLE) timeline** for Dash. Only small JSON travels over callbacks (the control plane); your media stays on the media plane (a `<video>`, an audio engine, a server render) driven *off* the timeline's outputs.

The package ships two components:

*   **`DashNleTimeline`** — frame ruler, stacked track lanes (`video` / `audio` / `camera`), clip rectangles, a draggable playhead, snapping, trim/resize/split/delete, waveforms from precomputed `peaks`, `ctrl/⌘ + wheel` zoom, keyboard shortcuts, and an imperative `command` prop.
*   **`DashNleScene`** — a Hybrid DOM + Canvas preview compositor driven by `playhead`, with overlay `layers`, a `tileset` background, and a `children` slot that pairs with [`dash-leaflet2`](https://github.com/pip-install-python/dash-leaflet2) for map-focused editing (fly a map from a camera keyframe track while compositing video on top).

### Quick Start

Frames — not seconds — are the canonical unit (`seconds = frame / fps`). Give the timeline an explicit height via `style`, define `tracks`, then place `clips` on them by `trackId`. Audio lanes paint waveforms from precomputed `peaks` — the component paints bars only, it never decodes audio.



```python
# File: docs/dash_nle_timeline/quick_start.py

import math

import dash_nle_timeline as nle
import dash_mantine_components as dmc

# Precomputed audio peaks (magnitude format: one positive value per bucket, 0..1).
# In a real app you would compute these server-side from the audio file.
PEAKS = [abs(math.sin(i / 7)) * 0.8 + 0.15 for i in range(220)]

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("A static three-track timeline — scrub the playhead, "
                     "drag clips, trim edges, or press S to split.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-quick-start-tl",
                fps=30,
                duration=300,
                pixelsPerSecond=90,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                    {"id": "v2", "kind": "video", "label": "V2"},
                    {"id": "a1", "kind": "audio", "label": "A1"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 90,
                     "label": "intro.mp4", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 100, "duration": 120,
                     "label": "main.mp4", "color": "teal"},
                    {"id": "c3", "trackId": "v2", "start": 60, "duration": 80,
                     "label": "overlay.png", "color": "orange"},
                    {"id": "c4", "trackId": "a1", "start": 0, "duration": 220,
                     "label": "music.wav",
                     "peaks": PEAKS, "peaksFormat": "magnitude", "peaksPerFrame": 1},
                ],
                markers=[
                    {"id": "m1", "frame": 100, "label": "cut", "color": "gold"},
                ],
                style={"width": "100%", "height": "260px"},
            ),
        ],
        gap="sm",
    ),
)
```


---

### Python Owns Clips — Apply lastEdit in a Callback

This is **the** core pattern of the component. Every committed edit (move, resize, trim, split, delete, select, seek) is reported as a single `lastEdit` dict. It carries a `timestamp`, so a Dash `Input` on it always fires — even for repeated identical edits.

The component applies edits to its own optimistic copy of `clips` (so they persist visually without a round-trip), but **Python is the source of truth**: a callback reads `lastEdit`, applies it to the clip list, and writes `clips` back. Writing back is how you validate, override, or persist edits — reject an edit by returning the unmodified list.



```python
# File: docs/dash_nle_timeline/edits_callback.py

import json

import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State, no_update

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("Move, resize, trim, or split a clip — every committed edit "
                     "arrives in Python as one `lastEdit` dict.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-edits-tl",
                fps=30,
                duration=240,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                    {"id": "v2", "kind": "video", "label": "V2"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 70,
                     "label": "shot A", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 90, "duration": 80,
                     "label": "shot B", "color": "teal"},
                    {"id": "c3", "trackId": "v2", "start": 40, "duration": 60,
                     "label": "title", "color": "orange"},
                ],
                style={"width": "100%", "height": "200px"},
            ),
            dmc.Text("Last edit event:", size="sm", fw=500),
            dmc.Code(id="nle-edits-out", block=True,
                     children="(interact with the timeline)"),
        ],
        gap="sm",
    ),
)


@callback(
    Output("nle-edits-out", "children"),
    Input("nle-edits-tl", "lastEdit"),
    prevent_initial_call=True,
)
def show_edit(edit):
    return json.dumps(edit, indent=2)


# Python owns `clips`: apply each reported edit to the list and feed it back.
# The component edits its own copy optimistically, so writing clips back is how
# you validate, override, or persist the result.
@callback(
    Output("nle-edits-tl", "clips"),
    Input("nle-edits-tl", "lastEdit"),
    State("nle-edits-tl", "clips"),
    prevent_initial_call=True,
)
def apply_edit(edit, clips):
    if not edit:
        return no_update
    by_id = {c["id"]: dict(c) for c in clips}
    edit_type = edit.get("type")
    clip = by_id.get(edit.get("clipId"))

    if edit_type == "move" and clip:
        clip["start"] = edit["start"]
        clip["trackId"] = edit.get("trackId", clip["trackId"])
    elif edit_type in ("resize", "trim") and clip:
        for key in ("start", "duration", "inPoint"):
            if key in edit:
                clip[key] = edit[key]
    elif edit_type == "delete" and clip:
        del by_id[clip["id"]]
    else:
        # select / seek / split / paste etc. — nothing to write back here
        return no_update
    return list(by_id.values())
```


---

### Playback and the Playhead

Setting `playing=True` starts a `requestAnimationFrame` loop that advances `playhead` at `fps` (writes throttled by `playheadThrottleMs`). The component plays no media itself — drive a real `<video>`, an audio engine, or a `DashNleScene` off the `playhead` output. The `playhead` prop round-trips: the component writes it on scrub/seek/playback, and a callback may write it to move the playhead programmatically. `loop=True` wraps back to frame 0 at `duration`.



```python
# File: docs/dash_nle_timeline/playback.py

import dash_nle_timeline as nle
import dash_mantine_components as dmc
from dash import callback, Input, Output, State

FPS = 30
DURATION = 240

component = dmc.Paper(
    withBorder=True,
    p="md",
    children=dmc.Stack(
        [
            dmc.Text("Playback is a requestAnimationFrame loop driven by the "
                     "`playing` prop — the component plays no media itself, it just "
                     "advances `playhead` at the configured fps.", size="sm", c="dimmed"),
            nle.DashNleTimeline(
                id="nle-playback-tl",
                fps=FPS,
                duration=DURATION,
                loop=True,
                tracks=[
                    {"id": "v1", "kind": "video", "label": "V1"},
                ],
                clips=[
                    {"id": "c1", "trackId": "v1", "start": 0, "duration": 100,
                     "label": "scene 1", "color": "indigo"},
                    {"id": "c2", "trackId": "v1", "start": 110, "duration": 110,
                     "label": "scene 2", "color": "teal"},
                ],
                style={"width": "100%", "height": "160px"},
            ),
            dmc.Group(
                [
                    dmc.Button("Play", id="nle-playback-btn", color="teal", size="sm"),
                    dmc.Text("frame 0  |  0.000s", id="nle-playback-out",
                             size="sm", ff="monospace", c="dimmed"),
                ],
                gap="md",
            ),
        ],
        gap="sm",
    ),
)


@callback(
    Output("nle-playback-tl", "playing"),
    Output("nle-playback-btn", "children"),
    Input("nle-playback-btn", "n_clicks"),
    State("nle-playback-tl", "playing"),
    prevent_initial_call=True,
)
def toggle_play(_n, playing):
    now_playing = not bool(playing)
    return now_playing, ("Pause" if now_playing else "Play")


@callback(
    Output("nle-playback-out", "children"),
    Input("nle-playback-tl", "playhead"),
)
def show_playhead(frame):
    frame = frame or 0
    return f"frame {frame}  |  {frame / FPS:.3f}s"
```


Keyboard shortcuts (enabled by default via `keyboard`): Space play/pause, arrows step (Shift = 1s), `S` split at playhead, `Delete`, `⌘/Ctrl+C/V/D` copy/paste/duplicate, `⌘/Ctrl+A` select all, Home/End, `+`/`-` zoom. The imperative `command` prop dispatches the same actions from Python (`play`, `pause`, `seek`, `splitAtPlayhead`, `zoomToFit`, …), de-duped by a unique `id`.

---

### DashNleScene

`DashNleScene` is the preview side: a compositor stage that renders, at the current `playhead`, a background — a `dash-leaflet2` `Map` in `children` and/or a native XYZ `tileset` — then overlay `layers` (image / video / text) with per-layer `[start, end)` visibility windows and a screen-space `camera`. Sync `playhead` and `playing` from a `DashNleTimeline`, and drive the `tileset.viewport` from a `camera`-kind track's `cameraKeyframes` to fly the map while compositing video on top.

```python
import dash_nle_timeline as nle

scene = nle.DashNleScene(
    id="nle-scene",
    fps=30,
    playhead=0,                       # sync from DashNleTimeline
    resolution=[1280, 720],
    tileset={
        "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
        "viewport": {"center": [48.8566, 2.3522], "zoom": 12},
    },
    layers=[
        {"id": "title", "kind": "text", "text": "Paris flyover",
         "rect": [0.05, 0.05, 0.9, 0.2], "start": 0, "end": 90},
    ],
    style={"width": "100%"},
)
```

For full map interactivity (pan/zoom/controls/`flyTo`) put a `dash-leaflet2` `Map` in `children` instead of using `tileset`.

---

### DashNleTimeline Props

| Property | Type | Default | Description |
|:---------|:-----|:--------|:------------|
| `id` | string | - | Component ID for Dash callbacks. |
| `fps` | number | `30` | Frames per second — defines the integer-frame grid and frame↔second conversions. |
| `duration` | number | `300` | Total timeline length in integer frames. |
| `playhead` | number | - | Current playhead position in integer frames. Written on scrub/seek/playback; writable from callbacks. |
| `playing` | boolean | - | When `True`, a rAF loop advances `playhead` at `fps`. The component plays no media itself. |
| `loop` | boolean | `False` | Loop back to 0 when the playhead reaches `duration`. |
| `playheadThrottleMs` | number | `60` | Min ms between `playhead` writes during playback. |
| `tracks` | list of dicts | - | Track lanes, top-to-bottom. Keys: `id`*, `kind` (`'video'`/`'audio'`/`'camera'`), `label`, `height`, `locked`, `muted`, `color`. |
| `clips` | list of dicts | - | Clips on tracks. Keys: `id`*, `trackId`*, `start`*, `duration`* (frames), `inPoint`, `label`, `color`, `src`, `peaks`, `peaksFormat` (`'minmax'`/`'magnitude'`), `peaksPerFrame`, `thumbnails`, `locked`. Python is the source of truth. |
| `lastEdit` | dict | - | [Readonly] Most recent committed edit. Keys: `type`* (`move`/`resize`/`trim`/`split`/`delete`/`select`/`seek`/`copy`/`paste`), `timestamp`*, `clipId`, `clipIds`, `trackId`, `start`, `duration`, `inPoint`, `frame`, `resultIds`, `snapped`. |
| `markers` | list of dicts | - | Labeled time points: visual guides + snap targets. Keys: `id`*, `frame`*, `label`, `color`. |
| `cameraKeyframes` | list of dicts | - | Keyframes for `'camera'` tracks. Keys: `id`*, `trackId`*, `time`*, `center`* `[lat, lng]`, `zoom`*, `bearing`, `easing`, `label`. |
| `selectedClipIds` | list of strings | - | Currently selected clip ids. Round-trips; writable from Python. |
| `selectedClipId` | string | - | [Readonly] The single selected clip id, or `None` when zero/many selected. |
| `command` | dict | - | Imperative command, de-duped by `id`. Keys: `id`*, `type`* (`play`/`pause`/`togglePlay`/`seek`/`stepFrame`/`splitAtPlayhead`/`deleteSelected`/`copy`/`paste`/`duplicate`/`selectAll`/`zoomToFit`/`zoomTo`/`scrollToFrame`), `payload`. |
| `pixelsPerSecond` | number | `120` | Horizontal zoom as pixels per second. |
| `minPixelsPerSecond` | number | `8` | Lower clamp for wheel-zoom. |
| `maxPixelsPerSecond` | number | `4000` | Upper clamp for wheel-zoom. |
| `scrollX` | number | - | Horizontal scroll offset of the lane viewport, in CSS px from frame 0. |
| `snapping` | boolean | `True` | Master enable for snapping during move/resize/trim. |
| `snapTargets` | list | `['clips','playhead','markers']` | Snap-eligible line categories: `'playhead'`, `'grid'`, `'markers'`, `'clips'`. |
| `snapThreshold` | number | `8` | Snap activation distance in screen px. |
| `gridFrames` | number | `0` | Grid spacing in frames for `'grid'` snapping. `0` = off. |
| `allowCrossTrackMove` | boolean | `True` | Allow dragging a clip onto another lane of the same kind. |
| `multiSelect` | boolean | `True` | Allow shift/cmd-click multi-selection. |
| `keyboard` | boolean | `True` | Enable all keyboard shortcuts. |
| `readOnly` | boolean | `False` | View-only: disable editing (scrub + selection + zoom still work). |
| `controls` | boolean | `True` | Show the built-in transport bar above the ruler. |
| `theme` | string | `'dark'` | Chrome color theme: `'dark'` or `'light'`. |
| `headerWidth` | number | `140` | Width of the left track-header column in px. |
| `rulerHeight` | number | `28` | Height of the time-ruler row in px. |
| `className` | string | - | CSS class name(s) for the root element. |
| `style` | object | - | Inline CSS styles — give the timeline an explicit width/height. |

### DashNleScene Props

| Property | Type | Default | Description |
|:---------|:-----|:--------|:------------|
| `id` | string | - | Component ID for Dash callbacks. |
| `children` | node | - | Background slot — e.g. a `dash-leaflet2` `Map`. Rendered behind the tileset and overlay layers, filling the stage. |
| `playhead` | number | - | Current playhead in integer frames (typically synced from a `DashNleTimeline`). Layers with a `[start, end)` window show/hide on it. |
| `playing` | boolean | `False` | When `True`, video layers play natively; when `False` they seek to `playhead / fps` (frame-accurate scrub). Sync from the timeline. |
| `fps` | number | `30` | Maps the playhead to seconds for video layers. |
| `layers` | list of dicts | - | Overlay layers, bottom-to-top by `z` then array order. Keys: `id`*, `kind` (`'image'`/`'video'`/`'text'`), `src`, `text`, `rect` `[x,y,w,h]` (0..1), `opacity`, `rotation`, `start`, `end`, `srcStart`, `z`, `color`, `fontSize`. |
| `tileset` | dict | - | Native XYZ tileset background. Keys: `url`*, `viewport`* (`{center: [lat,lng], zoom}`), `tileSize` (256), `maxZoom` (19), `opacity` (1), `subdomains` (`['a','b','c']`). |
| `camera` | dict | - | Screen-space pan/zoom of the overlay layer group: `{x: 0, y: 0, zoom: 1}`. |
| `resolution` | list | `[1280, 720]` | Output stage resolution `[width, height]` in px (sets the aspect ratio). |
| `background` | string | `'#000000'` | Stage background CSS color (behind everything). |
| `className` | string | - | CSS class name(s) for the root element. |
| `style` | object | - | Inline CSS styles for the component. |

*\* = required key inside the dict.*
