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.
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).
| Prop | Type | Default | Description |
|---|---|---|---|
| events | ZoomTimelineEvent[] | 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 | 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 | — | Fires from the press or key that selected an event, or cleared it (Escape) with null. |
| onViewChange | (view: ZoomTimelineView) => void | — | 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 | the middle of the events | Where the view opens. |
| defaultUnit | "year" | "month" | "day" | "hour" | "minute" | the ladder's second unit | The unit the view opens on. |
| levels | "years-minutes" | "years-days" | "months-hours" | "days-minutes" | "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 | 28 | How close markers get, in px, before they merge into a counted bubble (with 10% hysteresis); 0 never merges. |
| momentum | number | 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 | — | Draws a now line there. Passed in, never read from the clock, so the server and the browser agree. |
| utcOffset | number | 0 | Minutes added to UTC for every label and readout, e.g. 60 for UTC+1. |
| label | string | "Timeline" | The timeline's accessible name. |
| formatTime | (ms: number, unit: ZoomTimelineUnit) => string | "14 Mar 2026 14:20" | The readout's and every accessible name's time; the default drops the time at midnight. |
| zoomControls | boolean | true | Show the zoom out, unit and zoom in controls in the readout row. |
| height | number | 200 | Total height in px: a 36px readout row, the marker lane and a 52px axis with its context row. |
| accent | string | "var(--accent-bright)" | Selection, bubbles and the now line. Any CSS colour. |
| sound | boolean | 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 | false | Dims the timeline and stops every gesture and control. |
| className | string | — | Merged onto the root (a rounded, clipped container-query box). |