Skip to content

@vibe-labs/design-vue-timeline

Vue 3 components and composables for timeline-based editors. Wraps @vibe-labs/design-components-timeline CSS with behaviour, accessibility, provide/inject context, playback, drag/resize, selection, keyboard shortcuts, and zoom.

Zero CSS in this package. All visual styling comes from @vibe-labs/design-components-timeline.

Installation

bash
npm install @vibe-labs/design-vue-timeline

Requires the component-level CSS to be imported separately:

css
@import "@vibe-labs/design-components-timeline";

Quick Start

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  VibeTimeline,
  VibeTimelineRuler,
  VibeTimelineTrack,
  VibeTimelineBlock,
  VibeTimelineMarker,
  VibeTimelinePlayhead,
  VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";

const currentTime = ref(0);
const playing = ref(false);
</script>

<template>
  <VibeTimeline
    :duration="30"
    :current-time="currentTime"
    :playing="playing"
    density="normal"
    snap="quarter"
    snapping
  >
    <VibeTimelineRuler :major-interval="1" :minor-subdivisions="4" />

    <VibeTimelineTrack kind="dialogue" label="Bob">
      <VibeTimelineGrid :major-interval="1" />
      <VibeTimelineBlock
        variant="speech"
        :start="2"
        :duration="3.5"
        label="Hello there"
      />
      <VibeTimelineMarker variant="cue" :time="5.5" label="Blink" />
    </VibeTimelineTrack>

    <VibeTimelineTrack kind="event" label="Camera">
      <VibeTimelineGrid :major-interval="1" />
      <VibeTimelineBlock
        variant="gesture"
        :start="1"
        :duration="4"
        label="Pan right"
      />
    </VibeTimelineTrack>

    <VibeTimelinePlayhead />
  </VibeTimeline>
</template>

Components

<VibeTimeline>

Root container. Sets up the timeline context via provide/inject. All child components read coordinate conversion, snap settings, and playback state from this context.

PropTypeDefaultDescription
durationnumberrequiredTotal timeline duration (time units)
currentTimenumber0Playhead position
densityTimelineZoomDensity"normal"Horizontal zoom
snapTimelineSnapResolution"beat"Grid snap resolution
snappingbooleantrueWhether snapping is active
loopingbooleanfalseLoop mode
playingbooleanfalsePlayback active
pixelsPerUnitnumber100Base px/unit before density scaling
loopStartnumberLoop region start
loopEndnumberLoop region end
headerWidthstringOverride sidebar width (CSS value)
EventPayloadDescription
update:currentTimenumberPlayhead moved
update:playingbooleanPlayback toggled
update:densitystringZoom changed
seeknumberExplicit seek
SlotScoped PropsDescription
default{ timeline, contentWidth }All child content

Exposed: timeline (composable), el (root HTMLElement ref)


<VibeTimelineRuler>

Time axis along the top edge with tick marks.

PropTypeDefaultDescription
majorIntervalnumber1Major tick spacing (time units)
minorSubdivisionsnumber4Minor ticks per major
formatLabel(time: number) => stringmm:ss.sCustom tick label formatter

<VibeTimelineTrackGroup>

Collapsible group wrapping multiple tracks.

PropTypeDefaultDescription
labelstringrequiredGroup name
collapsedbooleanfalseWhether collapsed
idstringUnique identifier
EventPayload
update:collapsedboolean
toggle
SlotDescription
labelCustom group header
defaultChild tracks

<VibeTimelineTrack>

Single horizontal lane with header sidebar.

PropTypeDefaultDescription
kindTimelineTrackKind"audio"Track type (accent colour)
labelstringrequiredHeader display text
mutedbooleanfalseMuted state
solobooleanfalseSolo state
lockedbooleanfalseLocked (no editing)
collapsedbooleanfalseHeight collapsed
idstringUnique identifier
EventPayload
update:mutedboolean
update:soloboolean
update:lockedboolean
update:collapsedboolean
SlotDescription
headerCustom track header content
defaultLane content (blocks, markers, waveforms, grid)

<VibeTimelineBlock>

Positioned region with duration. Supports drag, resize, and selection.

PropTypeDefaultDescription
variantTimelineBlockVariant"generic"Colour variant
emphasisTimelineBlockEmphasis"primary"Visual weight
startnumberrequiredStart time (units)
durationnumberrequiredDuration (units)
labelstringDisplay text
selectedbooleanfalseSelection state
resizablebooleantrueShow resize handles
draggablebooleantrueAllow dragging
idstringUnique identifier
EventPayloadDescription
update:selectedbooleanSelection changed
selectPointerEventBlock clicked/tapped
dragstartPointerEvent, DragModeDrag initiated
clickPointerEventClick
dblclickMouseEventDouble-click (open editor)
SlotDescription
defaultCustom block content (replaces label)

<VibeTimelineMarker>

Point-in-time event with no duration.

PropTypeDefaultDescription
variantTimelineMarkerVariant"cue"Marker type/colour
timenumberrequiredPosition (units)
labelstringTooltip text
selectedbooleanfalseSelection state
idstringUnique identifier
EventPayload
update:selectedboolean
selectPointerEvent
clickPointerEvent
dblclickMouseEvent

<VibeTimelinePlayhead>

Vertical position indicator. Reads from timeline context by default.

PropTypeDefaultDescription
timenumbercontextOverride position
playingbooleancontextOverride playing state

<VibeTimelineGrid>

Background snap grid rendered via CSS gradients.

PropTypeDefaultDescription
majorIntervalnumber1Major line spacing
minorSubdivisionsnumber4Minor lines per major

<VibeTimelineWaveform>

Canvas-based audio waveform visualisation.

PropTypeDefaultDescription
waveformStyleTimelineWaveformStyle"bars"Render mode
peaksnumber[]requiredNormalised peak data (0–1)
startnumber0Position (units)
durationnumberDuration (units)
colorstringCustom colour override

<VibeTimelineSelection>

Range highlight region.

PropTypeDefaultDescription
startnumberRange start (units)
endnumberRange end (units)
activebooleanfalseWhether visible

Composables

useTimeline(options)

Core composable. Creates and provides the TimelineContext to all children. Called internally by <VibeTimeline> — you only need this directly if building a headless timeline.

Returns: TimelineContext + basePixelsPerUnit (writable ref).

useTimelinePlayback(options)

RAF-driven playback loop. Advances currentTime at real-time speed, handles looping and speed multiplier.

Returns: play(), pause(), togglePlayback(), seekTo(time), seekRelative(delta), rewind().

useTimelineDrag(options)

Block drag/move and edge resize with snap support. Uses pointer capture for reliable tracking.

Returns: state (reactive DragState), beginDrag(event, blockId, mode, start, duration).

useTimelineSelection(options)

Manages selected block/marker IDs and range selection.

Returns: selectedIds, select(id, additive?), clearSelection(), isSelected(id), beginRange(time), updateRange(time), endRange(), clearRange(), clearAll().

useTimelineKeyboard(options)

Keyboard shortcut handler.

ShortcutAction
SpacePlay/pause
Arrow keysSeek
HomeRewind
DeleteDelete selected
Ctrl+ASelect all
EscapeDeselect
Ctrl+± / WheelZoom
SToggle snap
LToggle loop
Ctrl+Z / Shift+ZUndo/redo

useTimelineZoom(options)

Zoom management with Ctrl+wheel support.

Returns: zoomIn(), zoomOut(), setZoom(ppu), zoomToFit(start, end, width), stepDensity(direction).


Provide/Inject

The root <VibeTimeline> provides a TimelineContext via the TIMELINE_INJECTION_KEY symbol. All child components inject this to read coordinate conversion, snap settings, and playback state:

ts
import { inject } from "vue";
import { TIMELINE_INJECTION_KEY } from "@vibe-labs/design-vue-timeline";

const ctx = inject(TIMELINE_INJECTION_KEY)!;
const px = ctx.timeToPixels(2.5); // time → pixel offset

Dependencies

PackageRelationship
@vibe-labs/design-components-timelineCSS + types
@vibe-labs/coreShared utilities
vueFramework (peer)

Build

bash
npm run build

Usage Guide

Setup

ts
import {
  VibeTimeline,
  VibeTimelineRuler,
  VibeTimelineTrack,
  VibeTimelineBlock,
  VibeTimelineMarker,
  VibeTimelinePlayhead,
  VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";
import "@vibe-labs/design-components-timeline";

VibeTimeline — Practical Examples

Basic Playback Timeline

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  VibeTimeline,
  VibeTimelineRuler,
  VibeTimelineTrack,
  VibeTimelineBlock,
  VibeTimelinePlayhead,
  VibeTimelineGrid,
  useTimelinePlayback,
} from "@vibe-labs/design-vue-timeline";

const currentTime = ref(0);
const playing = ref(false);
const duration = 60;

// Clip data
const clips = ref([
  { id: "c1", start: 0, duration: 8, label: "Intro" },
  { id: "c2", start: 10, duration: 15, label: "Main segment" },
  { id: "c3", start: 30, duration: 20, label: "Outro" },
]);
</script>

<template>
  <div>
    <button @click="playing = !playing">{{ playing ? "Pause" : "Play" }}</button>
    <button @click="currentTime = 0">Rewind</button>
    <span>{{ currentTime.toFixed(2) }}s</span>

    <VibeTimeline
      :duration="duration"
      v-model:current-time="currentTime"
      v-model:playing="playing"
      density="normal"
      snap="beat"
      snapping
    >
      <VibeTimelineRuler :major-interval="5" :minor-subdivisions="5" />

      <VibeTimelineTrack kind="video" label="Video">
        <VibeTimelineGrid :major-interval="5" />
        <VibeTimelineBlock
          v-for="clip in clips"
          :key="clip.id"
          :start="clip.start"
          :duration="clip.duration"
          :label="clip.label"
          variant="generic"
        />
      </VibeTimelineTrack>

      <VibeTimelinePlayhead />
    </VibeTimeline>
  </div>
</template>

Multi-Track Editor with Selection

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  VibeTimeline,
  VibeTimelineRuler,
  VibeTimelineTrackGroup,
  VibeTimelineTrack,
  VibeTimelineBlock,
  VibeTimelineMarker,
  VibeTimelinePlayhead,
  VibeTimelineGrid,
} from "@vibe-labs/design-vue-timeline";

const currentTime = ref(0);
const playing = ref(false);
const selectedBlockId = ref<string | null>(null);
</script>

<template>
  <VibeTimeline
    :duration="120"
    v-model:current-time="currentTime"
    v-model:playing="playing"
    density="normal"
    snapping
  >
    <VibeTimelineRuler :major-interval="10" :minor-subdivisions="4" />

    <VibeTimelineTrackGroup label="Dialogue">
      <VibeTimelineTrack kind="dialogue" label="Actor A">
        <VibeTimelineGrid :major-interval="10" />
        <VibeTimelineBlock
          id="b1"
          variant="speech"
          :start="5"
          :duration="8"
          label="Line 1"
          :selected="selectedBlockId === 'b1'"
          @select="selectedBlockId = 'b1'"
        />
      </VibeTimelineTrack>

      <VibeTimelineTrack kind="dialogue" label="Actor B">
        <VibeTimelineGrid :major-interval="10" />
        <VibeTimelineBlock
          id="b2"
          variant="speech"
          :start="15"
          :duration="6"
          label="Response"
          :selected="selectedBlockId === 'b2'"
          @select="selectedBlockId = 'b2'"
        />
      </VibeTimelineTrack>
    </VibeTimelineTrackGroup>

    <VibeTimelineTrack kind="event" label="Cues">
      <VibeTimelineMarker variant="cue" :time="10" label="Scene change" />
      <VibeTimelineMarker variant="cue" :time="25" label="Music in" />
    </VibeTimelineTrack>

    <VibeTimelinePlayhead />
  </VibeTimeline>
</template>

Waveform Track

vue
<script setup lang="ts">
import {
  VibeTimeline,
  VibeTimelineTrack,
  VibeTimelineWaveform,
  VibeTimelinePlayhead,
} from "@vibe-labs/design-vue-timeline";

// Normalised peaks array 0–1
const audioPeaks = Array.from({ length: 200 }, () => Math.random() * 0.8 + 0.1);
</script>

<template>
  <VibeTimeline :duration="30" :current-time="0" :playing="false">
    <VibeTimelineTrack kind="audio" label="Audio">
      <VibeTimelineWaveform
        :peaks="audioPeaks"
        :start="0"
        :duration="30"
        waveform-style="bars"
      />
    </VibeTimelineTrack>
    <VibeTimelinePlayhead />
  </VibeTimeline>
</template>

Composables — Usage Example

vue
<script setup lang="ts">
import { ref } from "vue";
import { useTimelinePlayback } from "@vibe-labs/design-vue-timeline";

const currentTime = ref(0);
const playing = ref(false);
const duration = 60;

const { play, pause, seekTo, rewind } = useTimelinePlayback({
  currentTime,
  playing,
  duration: ref(duration),
  looping: ref(false),
  loopStart: ref(0),
  loopEnd: ref(duration),
  speed: ref(1),
});
</script>

<template>
  <button @click="play">Play</button>
  <button @click="pause">Pause</button>
  <button @click="rewind">Rewind</button>
  <button @click="seekTo(30)">Jump to 30s</button>
</template>

Common Patterns

Inject Timeline Context in a Child Component

vue
<script setup lang="ts">
import { inject } from "vue";
import { TIMELINE_INJECTION_KEY } from "@vibe-labs/design-vue-timeline";

const ctx = inject(TIMELINE_INJECTION_KEY)!;

function getPixelOffset(timeInSeconds: number) {
  return ctx.timeToPixels(timeInSeconds);
}
</script>

Controlled Zoom Level

vue
<script setup lang="ts">
import { ref } from "vue";
import { VibeTimeline } from "@vibe-labs/design-vue-timeline";

const density = ref<"compact" | "normal" | "wide">("normal");
</script>

<template>
  <div>
    <button @click="density = 'compact'">Zoom Out</button>
    <button @click="density = 'normal'">Fit</button>
    <button @click="density = 'wide'">Zoom In</button>

    <VibeTimeline
      :duration="60"
      :current-time="0"
      :playing="false"
      v-model:density="density"
    >
      <!-- tracks -->
    </VibeTimeline>
  </div>
</template>

Vibe