KQ-1288 · ZOOM/TIMELINE

Zoom Timeline

A horizontal timeline with semantic zoom from years to minutes: Ctrl or Cmd with the wheel zooms about the pointer on the flick spring, a pinch zooms 1:1 about the fingers, and + and - step on glide, while each tick label fades in as the room around it grows (quarters before months, Mondays before days, every third hour before every hour) and the unit above becomes a row of context labels pinned to the left edge. Markers closer than the cluster radius merge into counted bubbles, and each member springs in and out on snap as you zoom, so a bubble visibly splits into its events; dragging pans 1:1 with rubber-banded ends, and a throw coasts on a critically damped spring whose time constant is the projection's, carrying the release velocity. Markers are one roving list: arrows step and pan to keep them in view, Enter selects an event or opens a bubble, Page Up and Page Down pan a screen.

KQ-1288 · ZOOM/TIMELINE
Ctrl or ⌘ + scroll to zoom · drag to panPinch to zoom · drag to pan

Arrow keys move between events, plus and minus zoom, Enter opens a bubble or selects an event, Page Up and Page Down pan.

drag to pan · ctrl + scroll to zoom

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/zoom-timeline

Props

events
ZoomTimelineEvent[]
default defaultTimelineEvents
The events to place: id, at (ms, an ISO string or a Date), title, detail and tone (default, success, warn or danger).
value / defaultValue
string | null
default null
The selected event. Its marker fills on flick and the readout row cross-fades to its title, time and detail.
onValueChange
(id: string | null) => void
default —
Fires from the press or key that selected an event, or cleared it (Escape) with null.
onViewChange
(view: ZoomTimelineView) => void
default —
The view once a gesture, step or coast settles: start and end in ms, the axis unit, and how many events and bubbles are in view.
defaultCenter
number | string | Date
default the middle of the events
Where the view opens.
defaultUnit
"year" | "month" | "day" | "hour" | "minute"
default the ladder's second unit
The unit the view opens on.
levels
"years-minutes" | "years-days" | "months-hours" | "days-minutes"
default "years-minutes"
The zoom ladder: how far out and in the view can go. Changing it re-frames the view on its opening unit.
cluster
number
default 28
How close markers get, in px, before they merge into a counted bubble (with 10% hysteresis); 0 never merges.
momentum
number
default 0.6
How far a throw coasts, 0 to 1: 0 stops almost under the finger, 1 glides a long way.
now
number | string | Date
default —
Draws a now line there. Passed in, never read from the clock, so the server and the browser agree.
utcOffset
number
default 0
Minutes added to UTC for every label and readout, e.g. 60 for UTC+1.
label
string
default "Timeline"
The timeline's accessible name.
formatTime
(ms: number, unit: ZoomTimelineUnit) => string
default "14 Mar 2026 14:20"
The readout's and every accessible name's time; the default drops the time at midnight.
zoomControls
boolean
default true
Show the zoom out, unit and zoom in controls in the readout row.
height
number
default 200
Total height in px: a 36px readout row, the marker lane and a 52px axis with its context row.
accent
string
default "var(--accent-bright)"
Selection, bubbles and the now line. Any CSS colour.
sound
boolean
default false
A tick each time the axis changes unit during a visitor's zoom and softly as arrow keys step, and a whoosh for a fast throw or a zoom step. Nothing plays on its own.
disabled
boolean
default false
Dims the timeline and stops every gesture and control.
className
string
default —
Merged onto the root (a rounded, clipped container-query box).