# @kteneyck/cesium-timeline

A canvas-based timeline component for **React** and **Angular** with Cesium Clock integration. Provides interactive time scrubbing, smooth edge-scroll, Netflix/Hulu-style playback controls, a LIVE indicator, and a flexible token-based datetime format system.

## Packages

| Package | Description | npm |
|---|---|---|
| `@kteneyck/cesium-timeline-core` | Framework-agnostic types, utils, canvas engine | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-core)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-core) |
| `@kteneyck/cesium-timeline-react` | React components (thin wrappers around core) | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-react)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-react) |
| `@kteneyck/cesium-timeline-angular` | Angular standalone components (Angular 17+) | [![npm](https://img.shields.io/npm/v/@kteneyck/cesium-timeline-angular)](https://www.npmjs.com/package/@kteneyck/cesium-timeline-angular) |

---

## Installation

### React

```bash
npm install @kteneyck/cesium-timeline-react
```

Peer dependencies: `react` ≥ 19, `cesium` ≥ 1.100

### Angular

```bash
npm install @kteneyck/cesium-timeline-angular
```

Peer dependencies: `@angular/core` ≥ 17, `cesium` ≥ 1.100

---

## Basic Usage

### React

```tsx
import { Timeline } from '@kteneyck/cesium-timeline-react';

const MyComponent = () => {
  const viewer = /* your Cesium Viewer */;

  return (
    <Timeline
      clock={viewer.clock}
      height={120}
      onTimeChange={(t) => { viewer.clock.currentTime = t; }}
      onPlayPause={(playing) => { viewer.clock.shouldAnimate = playing; }}
      onMultiplierChange={(m) => { viewer.clock.multiplier = m; }}
    />
  );
};
```

When `clock` is provided the component subscribes to `clock.onTick` and stays in sync automatically. All `onTimeChange`, `onPlayPause`, and `onMultiplierChange` callbacks are optional — the controls still work without them if you only pass `clock`.

### Without a Cesium Clock

```tsx
<Timeline
  startTime={new Date('2026-01-01T00:00:00')}
  endTime={new Date('2026-12-31T23:59:59')}
  height={80}
  showControls={false}
  onTimeChange={(t) => console.log(Cesium.JulianDate.toIso8601(t))}
/>
```

When no `clock` is provided the component falls back to `setInterval` and tracks real wall-clock time.

### Angular

```typescript
import { Component } from '@angular/core';
import { TimelineComponent } from '@kteneyck/cesium-timeline-angular';
import * as Cesium from 'cesium';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TimelineComponent],
  template: `
    <ct-timeline
      [clock]="viewer.clock"
      [height]="120"
      (timeChange)="onTimeChange($event)"
      (playPause)="onPlayPause($event)"
      (multiplierChange)="onMultiplierChange($event)"
    />
  `,
})
export class AppComponent {
  viewer!: Cesium.Viewer;

  onTimeChange(t: Cesium.JulianDate) {
    this.viewer.clock.currentTime = t;
  }

  onPlayPause(playing: boolean) {
    this.viewer.clock.shouldAnimate = playing;
  }

  onMultiplierChange(m: number) {
    this.viewer.clock.multiplier = m;
  }
}
```

Angular components use standalone imports — no NgModule required. Selectors: `ct-timeline`, `ct-timeline-canvas`, `ct-timeline-controls`.

---

## Features

- **Canvas rendering** — zero framework re-renders during playback or drag; all mutable state lives in refs/class properties.
- **Shared core engine** — identical visual output in React and Angular via `@kteneyck/cesium-timeline-core`.
- **Cesium Clock sync** — subscribes to `clock.onTick`; respects `shouldAnimate`, `multiplier`, and `currentTime`.
- **Draggable needle** — grab and drag the current-time indicator to scrub; cursor changes to `grab`/`grabbing`.
- **Click-to-seek** — click anywhere on the timeline to jump to that time.
- **Edge scroll** — drag the needle within 8% of either edge and the visible window scrolls smoothly underneath. The needle stays pinned to the cursor position as the window shifts.
- **Auto-scroll during playback** — visible window pans automatically when the needle reaches 10% from either edge.
- **Infinite scrolling window** — by default the timeline is not clamped to `startTime`/`endTime`; the window can pan anywhere. Set `restrictToRange` to hard-limit panning/zooming to those bounds instead.
- **Adaptive tick labels** — label granularity adapts to zoom level: milliseconds → seconds → HH:MM:SS → HH:MM → Month Day → Month Year → Year. Tick dates are shown only when the visible window spans more than 24 hours.
- **Configurable timezone** — tick labels and the datetime display can show any IANA timezone (e.g. `"UTC"`, `"America/New_York"`) or the browser's local time. A short abbreviation (e.g. `UTC`, `EST`, `PDT`) is displayed to the right of the date line whenever a non-local timezone is active.
- **Netflix/Hulu-style controls** — transport buttons (⏮ ◀◀ ▶/⏸ ▶▶ ⏭) always stay centered; speed badge and LIVE button in the left column never cause layout shift.
- **Conditional start/end buttons** — ⏮ and ⏭ are only rendered when `startTime` and `endTime` props are explicitly provided.
- **Speed cycling** — FF cycles through `ffSpeeds` (default `2×→4×→8×→16×→32×→1×`); RW cycles through `rwSpeeds` (default `−1×→−2×→−4×→−8×→−16×→−32×`). Both arrays are fully configurable.
- **Playback-speed overlay** — click the speed badge to open a popover with a slider and a number input for setting an exact speed within `minSpeed`–`maxSpeed`.
- **LIVE button** — shows a red dot + filled background when within 10 s of wall clock; dim outline otherwise. Clicking jumps to `Date.now()` and resets speed to 1×. Configurable size (`sm`/`md`/`lg`) and position (`left`/`right`). Dot color is themeable via `liveDotColor`.
- **Speed badge** — shown in the left column when multiplier ≠ 1×; click to open the playback-speed overlay (slider + input).
- **Two-line datetime display** — time displayed large/bold; date displayed smaller in the theme's active color.
- **Clickable datetime** — pass `onDateTimeClick` to open your own date picker; pass the result back via `jumpToTime` to pan the canvas and set the time.
- **Token-based datetime format** — built-in presets plus custom format strings with 17 supported tokens.
- **Max tick limit** — `maxTicks` prop prevents the canvas from becoming overloaded at wide zoom levels by coarsening the tick scale automatically.
- **Ghost / preview needle** — while hovering over the timeline a semi-transparent (15% opacity) preview needle tracks the cursor so you can see exactly where a click or zoom-to-selection drag would land before committing. The ghost disappears when the cursor leaves the canvas or an interaction begins.
- **Zoom to selection** — drag in the tick area (away from the needle) to draw a time-range highlight with a crosshair cursor; on release the visible window zooms to exactly the selected span and fires `onRangeSelect` (React) / `rangeSelect` (Angular) with the resulting start and end times.
- **Swim lanes** — display time intervals and instants as horizontal rows inside the canvas. Supports customizable styling, click/hover/double-click event hooks, drag-to-reorder, and vertical scrolling when lanes overflow.
- **Fully themeable** — 16 theme properties cover every color, size, and font setting, including swim lane item border defaults.
- **Localizable labels** — every control-bar label and tooltip is overridable via the `labels` prop; dynamic tooltips accept a `(multiplier: number) => string` callback. See [Labels & i18n](#labels--i18n).
- **Responsive** — fills container width; `ResizeObserver` redraws on resize.

---

## Props

### `TimelineProps`

| Prop | Type | Default | Description                                                                                                                                                                                                                             |
|------|------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `clock` | `Cesium.Clock` | — | Cesium clock to sync with. Falls back to `setInterval` if omitted.                                                                                                                                                                      |
| `startTime` | `JulianDate \| Date` | now − 12 h | Left bound of initial visible window. Also shows the ⏮ button when provided.                                                                                                                                                            |
| `endTime` | `JulianDate \| Date` | now + 12 h | Right bound of initial visible window. Also shows the ⏭ button when provided.                                                                                                                                                           |
| `currentTime` | `JulianDate \| Date` | `startTime` | Initial needle position                                                                                                                                                                                                                 |
| `height` | `number` | `120` | Canvas height in pixels                                                                                                                                                                                                                 |
| `showControls` | `boolean` | `true` | Show/hide the control bar                                                                                                                                                                                                               |
| `enableDrag` | `boolean` | `true` | Show/hide the canvas (drag/seek area)                                                                                                                                                                                                   |
| `showLabels` | `boolean` | — | Show/hide tick labels on the canvas                                                                                                                                                                                                     |
| `snapToTicks` | `boolean` | — | Snap needle to nearest tick on drag                                                                                                                                                                                                     |
| `tickInterval` | `TickInterval \| number` | auto | Override automatic tick interval                                                                                                                                                                                                        |
| `maxTicks` | `number` | unlimited | Maximum number of major ticks on the canvas at once. When exceeded the tick scale is automatically coarsened.                                                                                                                           |
| `ffSpeeds` | `number[]` | `[2,4,8,16,32,1]` | Speed steps cycled by the ▶▶ button. Last entry wraps back to first.                                                                                                                                                                    |
| `rwSpeeds` | `number[]` | `[1,2,4,8,16,32]` | Absolute-value speed steps cycled by the ◀◀ button (negated internally).                                                                                                                                                                |
| `minSpeed` | `number` | `1` | Lower bound of the slider/input in the playback-speed overlay (opened by clicking the speed badge).                                                                                                                                     |
| `maxSpeed` | `number` | `100` | Upper bound of the slider/input in the playback-speed overlay.                                                                                                                                                                          |
| `dateTimeFormat` | `string` | `'MMM DD YYYY HH:mm:ss'` | Token-based format string for the controls datetime display                                                                                                                                                                             |
| `timezone` | `string` | browser local | IANA timezone name (e.g. `'UTC'`, `'America/New_York'`) or `'local'` for the browser's timezone. Controls both tick labels and the datetime display. When set, a short abbreviation (e.g. `UTC`, `EST`) appears to the right of the date. |
| `onDateTimeClick` | `() => void` | — | Called when the user clicks the datetime display. Use to open your own date picker.                                                                                                                                                     |
| `jumpToTime` | `JulianDate \| Date` | — | Set to programmatically jump the timeline to a moment (pans canvas + sets time).                                                                                                                                                        |
| `theme` | `Partial<TimelineTheme>` | `defaultTheme` | Theme overrides (merged with defaults)                                                                                                                                                                                                  |
| `className` | `string` | — | CSS class applied to the root div                                                                                                                                                                                                       |
| `onTimeChange` | `(t: JulianDate) => void` | — | Fires when needle moves (drag, click, or clock tick)                                                                                                                                                                                    |
| `onPlayPause` | `(playing: boolean) => void` | — | Fires on play/pause toggle                                                                                                                                                                                                              |
| `onMultiplierChange` | `(m: number) => void` | — | Fires when speed changes                                                                                                                                                                                                                |
| `swimLanes` | `SwimLane[]` | — | Array of swim lane definitions to render on the canvas                                                                                                                                                                                  |
| `showSwimLanes` | `boolean` | `true` | Show or hide the swim lanes                                                                                                                                                                                                             |
| `onSwimLaneItemClick` | `(info: SwimLaneEventInfo) => void` | — | Fires when a swim lane item is clicked                                                                                                                                                                                                  |
| `onSwimLaneItemHover` | `(info: SwimLaneEventInfo \| null) => void` | — | Fires when mouse enters/leaves a swim lane item                                                                                                                                                                                         |
| `onSwimLaneItemDoubleClick` | `(info: SwimLaneEventInfo) => void` | — | Fires when a swim lane item is double-clicked                                                                                                                                                                                           |
| `onSwimLaneReorder` | `(orderedIds: string[]) => void` | — | Fires when swim lanes are reordered via drag. Receives the new lane id order.                                                                                                                                                           |
| `onRangeSelect` | `(start: JulianDate, end: JulianDate) => void` | — | Fires when the user completes a click-and-drag in the tick area. The visible window zooms to the selected span; receives the resulting start and end times.                                                                             |
| `labels` | `Partial<TimelineLabels>` | English defaults | Override any control-bar label or tooltip string. See [Labels & i18n](#labels--i18n).                                                                                                                                                   |
| `liveButtonSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the LIVE button in the control bar.                                                                                                                                                                                             |
| `liveButtonPosition` | `'left' \| 'right'` | `'left'` | Position of the LIVE button — beside the datetime display (`'left'`) or the opposite side of the control bar (`'right'`).                                                                                                               |
| `showLive` | `boolean` | `true` | Show or hide the LIVE button in the control bar.                                                                                                                                                                                        |
| `invertScrollZoom` | `boolean` | `false` | Reverses the scroll-wheel zoom direction. By default scroll up zooms in and scroll down zooms out. Set to `true` for scroll down zooms in, scroll up zooms out.                                                                |
| `restrictToRange` | `boolean` | `false` | When `true`, `startTime`/`endTime` become hard scroll/zoom limits — drag, wheel, pinch, edge-scroll, and zoom-to-selection can no longer move the visible window past them. Has no effect unless `startTime` and/or `endTime` are provided. |

---

## Theme

Pass a partial `TimelineTheme` object to the `theme` prop. Any omitted properties fall back to `defaultTheme`.

```tsx
<Timeline
  clock={viewer.clock}
  theme={{
    backgroundColor: '#111',
    indicatorColor: '#ffd54f',
    buttonActiveColor: '#ffd54f',
  }}
/>
```

### `TimelineTheme` Properties

| Property | Default   | Description |
|----------|-----------|-------------|
| `backgroundColor` | `#1a1a1a` | Canvas background colour |
| `tickColor` | `#666`    | Minor tick stroke colour |
| `majorTickColor` | `#999`    | Major tick stroke colour |
| `labelColor` | `#ccc`    | Tick label and datetime text colour |
| `indicatorColor` | `#d69826` | Needle (current-time line) colour |
| `indicatorLineWidth` | `5`       | Needle stroke width in px |
| `majorTickHeight` | `10`      | Major tick height in px |
| `minorTickHeight` | `5`       | Minor tick height in px |
| `fontSize` | `12`      | Tick label font size in px |
| `controlBarBackground` | `#242424` | Control bar background colour |
| `controlBarBorder` | `#333`    | Control bar bottom border colour |
| `buttonColor` | `#666`    | Normal button colour |
| `buttonHoverColor` | `#888`    | Button hover colour |
| `buttonActiveColor` | `#d69826` | Active buttons, LIVE, speed badge, and date line colour |
| `swimLaneItemBorderColor` | `#666666` | Default border colour for swim lane interval bars. Can be overridden per-lane or per-item. |
| `swimLaneItemBorderWidth` | `0`       | Default border width (px) for swim lane interval bars. Set to `0` to remove borders globally. Can be overridden per-lane or per-item. |
| `liveDotColor` | `#e53e3e` | Colour of the dot shown on the LIVE button when playback is live. |

> **Note:** Theme colours must be resolved hex/rgb values. CSS variables like `var(--primary-color)` do **not** work in canvas `ctx.fillStyle`. Use `getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim()` to resolve them first.

### Resolving CSS Variables

```tsx
const theme = useMemo(() => {
  const style = getComputedStyle(document.documentElement);
  return {
    indicatorColor: style.getPropertyValue('--primary-color').trim() || '#ffd54f',
    backgroundColor: style.getPropertyValue('--surface-ground').trim() || '#1a1a1a',
  };
}, []);

<Timeline clock={viewer.clock} theme={theme} />
```

---

## Timezone

By default the timeline displays all times in the **browser's local timezone**. Pass the `timezone` prop to use UTC or any [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) instead.

```tsx
// UTC
<Timeline clock={viewer.clock} timezone="UTC" ... />

// A named IANA zone
<Timeline clock={viewer.clock} timezone="America/New_York" ... />

// Back to local (or simply omit the prop)
<Timeline clock={viewer.clock} timezone="local" ... />
```

Both the **canvas tick labels** and the **control bar datetime display** update to reflect the chosen timezone. When a non-local timezone is active a short abbreviation (e.g. `UTC`, `EST`, `PDT`) is shown to the right of the date line. The abbreviation is DST-aware — it automatically switches between `EST` and `EDT`, `PST` and `PDT`, etc.

### `Timezones` constants

```tsx
import { Timezones } from '@kteneyck/cesium-timeline-react';

<Timeline timezone={Timezones.UTC} ... />   // "UTC"
<Timeline timezone={Timezones.LOCAL} ... /> // "local" (default behavior)
```

### `getTimezoneAbbr` utility

Returns the short abbreviation for a given date and timezone, or `null` for local.

```tsx
import { getTimezoneAbbr } from '@kteneyck/cesium-timeline-react';

getTimezoneAbbr(new Date(), 'UTC');               // → "UTC"
getTimezoneAbbr(new Date(), 'America/Chicago');   // → "CDT" or "CST"
getTimezoneAbbr(new Date());                      // → null  (local)
```

### `formatDateTime` with timezone

The `formatDateTime` utility accepts an optional third argument:

```tsx
import { formatDateTime, DateTimeFormats } from '@kteneyck/cesium-timeline-react';

formatDateTime(new Date(), DateTimeFormats.ISO, 'UTC');
// → "2026-02-24 14:04:07"  (always in UTC regardless of local browser timezone)

formatDateTime(new Date(), DateTimeFormats.DEFAULT, 'America/Los_Angeles');
// → "Feb 24 2026 06:04:07"
```

---

## DateTime Format

The `dateTimeFormat` prop controls the two-line datetime display in the control bar. It accepts a token-based format string.

### Built-in Presets (`DateTimeFormats`)

```tsx
import { DateTimeFormats } from '@kteneyck/cesium-timeline-react';

<Timeline dateTimeFormat={DateTimeFormats.TWELVE_HR} ... />
```

| Key | Format string | Example output |
|-----|--------------|----------------|
| `DEFAULT` | `MMM DD YYYY HH:mm:ss` | Feb 24 2026 14:04:07 |
| `TWELVE_HR` | `MMM DD YYYY hh:mm:ss A` | Feb 24 2026 02:04:07 PM |
| `ISO` | `YYYY-MM-DD HH:mm:ss` | 2026-02-24 14:04:07 |
| `US` | `MM/DD/YYYY HH:mm` | 02/24/2026 14:04 |
| `EU` | `DD/MM/YYYY HH:mm` | 24/02/2026 14:04 |
| `TIME_ONLY` | `HH:mm:ss` | 14:04:07 |
| `TIME_12` | `hh:mm:ss A` | 02:04:07 PM |

### Supported Tokens

| Token | Example | Description |
|-------|---------|-------------|
| `YYYY` | 2026 | 4-digit year |
| `YY` | 26 | 2-digit year |
| `MMMM` | February | Full month name |
| `MMM` | Feb | Abbreviated month name |
| `MM` | 02 | Zero-padded month number |
| `M` | 2 | Month number |
| `DD` | 05 | Zero-padded day |
| `D` | 5 | Day |
| `HH` | 14 | 24-hour, zero-padded |
| `H` | 14 | 24-hour |
| `hh` | 02 | 12-hour, zero-padded |
| `h` | 2 | 12-hour |
| `mm` | 04 | Minutes, zero-padded |
| `ss` | 07 | Seconds, zero-padded |
| `SSS` | 042 | Milliseconds, zero-padded |
| `A` | PM | AM/PM uppercase |
| `a` | pm | AM/PM lowercase |

The control bar automatically splits the format string into a **time line** (large/bold) and a **date line** (smaller, in `buttonActiveColor`) using the `splitForDisplay` utility.

### `formatDateTime` Utility

```tsx
import { formatDateTime, DateTimeFormats } from '@kteneyck/cesium-timeline-react';

const label = formatDateTime(julianDate, DateTimeFormats.ISO);
// → "2026-02-24 14:04:07"

const label2 = formatDateTime(new Date(), 'DD/MM/YYYY HH:mm');
// → "24/02/2026 14:04"
```

---

## Playback Controls

The control bar uses a 3-column CSS grid so the transport buttons are always centered regardless of the content in the left (datetime/LIVE/badge) or right (empty spacer) columns.

### Transport Buttons

| Button | Action |
|--------|--------|
| ⏮ | Jump to `startTime` — **only rendered when `startTime` prop is provided** |
| ◀◀ | Cycle reverse speeds through `rwSpeeds` (wraps) |
| ▶ / ⏸ | Play / Pause. If coming out of reverse, resets to 1× forward. |
| ▶▶ | Cycle forward speeds through `ffSpeeds` (wraps) |
| ⏭ | Jump to `endTime` — **only rendered when `endTime` prop is provided** |

### LIVE Button

- Shows a red dot and filled background when the current time is within 2 seconds of `Date.now()`.
- Shows `LIVE` (dim outline, no dot) otherwise.
- Clicking jumps to `Date.now()`, centers the visible window, and resets speed to 1×.
- **Size** — controlled by the `liveButtonSize` prop: `'sm'` | `'md'` (default) | `'lg'`.
- **Position** — controlled by the `liveButtonPosition` prop: `'left'` (default, beside the datetime display) | `'right'` (right side of the control bar).
- **Visibility** — controlled by the `showLive` prop: `true` (default) shows the button, `false` hides it entirely.
- **Dot color** — controlled by `theme.liveDotColor` (default `#e53e3e`).

### Speed Badge

- Appears inline beside the LIVE button when multiplier ≠ 1×.
- Shows `◀ N×` for reverse, `N× ▶` for fast-forward.
- Clicking opens the playback-speed overlay (see below).

### Playback Speed Overlay

- Opened above the speed badge (so it stays visible when the timeline sits near the bottom of the page); closes on outside click, `Escape`, or when speed returns to 1×.
- Positioned with `position: fixed` at a very high z-index, computed from the badge's on-screen coordinates — it escapes any ancestor's `overflow: hidden` clipping and renders above other page content (e.g. a Cesium `Viewer` globe placed elsewhere on the page).
- Contains a range slider bounded by `minSpeed`–`maxSpeed` (default `1`–`100`), and a number input for typing an exact value.
- The number input accepts any typed value, including outside `minSpeed`–`maxSpeed` — it's applied as-is (not clamped) when the input loses focus or Enter is pressed, letting you exceed the slider's range.
- Direction (forward vs. reverse) is preserved — the overlay only changes the magnitude.
- A "Reset" button inside the overlay (with a tooltip explaining it resets to 1×) resets speed and closes it.

### Configuring Playback Speeds

```tsx
// Gentle: 2× and 4× only
<Timeline ffSpeeds={[2, 4, 1]} rwSpeeds={[1, 2, 4]} ... />

// Scientific: fine-grained control
<Timeline ffSpeeds={[10, 100, 1000, 10000, 1]} rwSpeeds={[1, 10, 100, 1000]} ... />
```

---

## Date Picker Integration

Pass `onDateTimeClick` to make the datetime display clickable. When clicked, open your own picker. Pass the result back as `jumpToTime` to pan the canvas to the selected time.

```tsx
const [jumpToTime, setJumpToTime] = useState<Date | undefined>();
const [pickerOpen, setPickerOpen] = useState(false);

<Timeline
  clock={viewer.clock}
  onDateTimeClick={() => setPickerOpen(true)}
  jumpToTime={jumpToTime}
  ...
/>

{/* Your picker — any library works */}
{pickerOpen && (
  <MyDatePicker
    onSelect={(date) => {
      setJumpToTime(date);
      setPickerOpen(false);
    }}
  />
)}
```

> `jumpToTime` is edge-triggered: the timeline reacts whenever the value changes. Wrap updates in a new `Date` instance (or use state) to ensure React detects the change.

---

## Labels & i18n

Every label and tooltip in the control bar is overridable via the `labels` prop. Pass a `Partial<TimelineLabels>` object — only the keys you provide are changed; everything else falls back to the English defaults.

### `TimelineLabels` reference

| Key | Default (English) | Notes |
|-----|-------------------|-------|
| `dateTimeClickTooltip` | `"Click to jump to a date/time"` | Tooltip on the datetime display when `onDateTimeClick` is wired up |
| `liveLabel` | `"LIVE"` | LIVE button text when not at live time |
| `liveActiveLabel` | `"LIVE"` | LIVE button text when at live time (red dot is rendered separately) |
| `liveTooltip` | `"Jump to live (now)"` | LIVE button tooltip when not at live time |
| `liveActiveTooltip` | `"Currently live"` | LIVE button tooltip when at live time |
| `resetSpeedTooltip` | `"Playback speed — click to adjust"` | Tooltip on the speed badge (opens the playback-speed overlay) |
| `speedOverlayTitle` | `"Playback speed"` | Heading at the top of the playback-speed overlay |
| `speedInputLabel` | `"Speed multiplier"` | Shared aria-label for the overlay's slider and number input |
| `resetSpeedLabel` | `"Reset"` | Visible text on the reset button inside the playback-speed overlay |
| `resetSpeedTitle` | `"Reset playback speed to 1×"` | Tooltip on the reset button inside the playback-speed overlay |
| `jumpToStartTooltip` | `"Jump to start"` | ⏮ button tooltip when a start time is set |
| `noStartTimeTooltip` | `"No start time set"` | ⏮ button tooltip when no start time is set |
| `jumpToEndTooltip` | `"Jump to end"` | ⏭ button tooltip when an end time is set |
| `noEndTimeTooltip` | `"No end time set"` | ⏭ button tooltip when no end time is set |
| `rewindTooltip` | `"Rewind"` | ◀◀ button tooltip at normal speed |
| `rewindActiveTooltip` | `(n) => "Reverse N× — click to speed up…"` | ◀◀ button tooltip while rewinding — receives the current multiplier |
| `playTooltip` | `"Play"` | ▶ button tooltip when stopped |
| `playFromRewindTooltip` | `"Play (reset to 1×)"` | ▶ button tooltip when coming out of rewind |
| `pauseTooltip` | `"Pause"` | ▶ button tooltip when playing |
| `fastForwardTooltip` | `"Fast forward"` | ▶▶ button tooltip at normal speed |
| `fastForwardActiveTooltip` | `(n) => "N× speed — click to increase…"` | ▶▶ button tooltip while fast-forwarding — receives the current multiplier |
| `collapseSwimLanesTooltip` | `"Collapse swim lanes"` | Chevron button tooltip when lanes are visible |
| `expandSwimLanesTooltip` | `"Expand swim lanes"` | Chevron button tooltip when lanes are hidden |

Dynamic fields (`rewindActiveTooltip`, `fastForwardActiveTooltip`) accept either a **static string** or a **function** `(multiplier: number) => string`. Use a function when you want to embed the speed value in your translated string.

### React example

```tsx
import { Timeline } from '@kteneyck/cesium-timeline-react';
import type { TimelineLabels } from '@kteneyck/cesium-timeline-core';

const frLabels: Partial<TimelineLabels> = {
  playTooltip: 'Lecture',
  pauseTooltip: 'Pause',
  liveLabel: 'EN DIRECT',
  liveActiveLabel: 'EN DIRECT',
  liveTooltip: 'Aller en direct',
  liveActiveTooltip: 'Vous êtes en direct',
  rewindTooltip: 'Retour rapide',
  rewindActiveTooltip: (n) => `Retour ${n}× — cliquer pour accélérer`,
  fastForwardTooltip: 'Avance rapide',
  fastForwardActiveTooltip: (n) => `${n}× — cliquer pour augmenter la vitesse`,
  collapseSwimLanesTooltip: 'Réduire les pistes',
  expandSwimLanesTooltip: 'Développer les pistes',
};

<Timeline clock={viewer.clock} labels={frLabels} height={120} />
```

### Angular example

```typescript
// component.ts
import type { TimelineLabels } from '@kteneyck/cesium-timeline-core';

@Component({ ... })
export class AppComponent {
  frLabels: Partial<TimelineLabels> = {
    playTooltip: 'Lecture',
    pauseTooltip: 'Pause',
    liveLabel: 'EN DIRECT',
    liveActiveLabel: 'EN DIRECT',
    liveTooltip: 'Aller en direct',
    liveActiveTooltip: 'Vous êtes en direct',
    rewindActiveTooltip: (n) => `Retour ${n}× — cliquer pour accélérer`,
    fastForwardActiveTooltip: (n) => `${n}× — cliquer pour augmenter la vitesse`,
  };
}
```

```html
<!-- component.html -->
<ct-timeline [clock]="clock" [labels]="frLabels" [height]="120" />
```

---

## Exports

### React

```tsx
import {
  Timeline,          // Main component
  TimelineCanvas,    // Canvas component (imperative handle)
  TimelineControls,  // Transport controls
  TimelineSVG,       // SVG-based alternative renderer
} from '@kteneyck/cesium-timeline-react';
```

### Angular

```typescript
import {
  TimelineComponent,         // <ct-timeline>
  TimelineCanvasComponent,   // <ct-timeline-canvas>
  TimelineControlsComponent, // <ct-timeline-controls>
} from '@kteneyck/cesium-timeline-angular';
```

### Core (re-exported by both React and Angular packages)

```tsx
import {
  DateTimeFormats,   // Format string presets
  Timezones,         // { LOCAL: 'local', UTC: 'UTC' } convenience constants
  DEFAULT_LABELS,    // Default English label/tooltip strings
  formatDateTime,    // Token-based date formatter (date, format, timezone?)
  getTimezoneAbbr,   // Short timezone abbreviation for a date (date, timezone?)
  splitForDisplay,   // Split format string into time/date parts
  toJulianDate,      // Convert Date | JulianDate → JulianDate
  toDate,            // Convert Date | JulianDate → Date
  toMilliseconds,    // Convert ms → number
  fromMilliseconds,  // Convert ms → JulianDate
  getDurationMs,     // Duration between two dates in ms
  TickInterval,      // Enum: FIFTEEN_MIN | THIRTY_MIN | HOURLY | CUSTOM
  defaultTheme,      // Default theme values
  defaultSwimLaneStyle,
  DEFAULT_LANE_HEIGHT,
} from '@kteneyck/cesium-timeline-core';

// TypeScript types
import type {
  TimelineTheme,
  TimelineLabels,
  SwimLane,
  SwimLaneItem,
  SwimLaneItemStyle,
  SwimLaneStyle,
  SwimLaneEventInfo,
} from '@kteneyck/cesium-timeline-core';
```

---

## Examples

### Full Cesium Integration

```tsx
import { useRef, useEffect, useMemo, useState } from 'react';
import * as Cesium from 'cesium';
import { Timeline, DateTimeFormats } from '@kteneyck/cesium-timeline-react';

const CesiumWithTimeline = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [clock, setClock] = useState<Cesium.Clock | undefined>();

  useEffect(() => {
    if (!containerRef.current) return;
    const viewer = new Cesium.Viewer(containerRef.current);
    viewer.clock.shouldAnimate = false;
    setClock(viewer.clock);
    return () => viewer.destroy();
  }, []);

  const theme = useMemo(() => ({
    indicatorColor: '#ffd54f',
    buttonActiveColor: '#ffd54f',
    backgroundColor: '#1a1a1a',
  }), []);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
      <div ref={containerRef} style={{ flex: 1 }} />
      {clock && (
        <Timeline
          clock={clock}
          height={120}
          showControls={true}
          showLabels={true}
          snapToTicks={false}
          dateTimeFormat={DateTimeFormats.DEFAULT}
          theme={theme}
          onTimeChange={(t) => { clock.currentTime = t; }}
          onPlayPause={(playing) => { clock.shouldAnimate = playing; }}
          onMultiplierChange={(m) => { clock.multiplier = m; }}
          onRangeSelect={(start, end) => {
            // Zoom the Cesium clock range to match the selected span
            clock.startTime = start;
            clock.stopTime  = end;
            clock.currentTime = start;
          }}
        />
      )}
    </div>
  );
};
```

### With Start / End Bounds

Providing `startTime` and `endTime` shows the ⏮ and ⏭ jump buttons.

```tsx
const start = Cesium.JulianDate.fromIso8601('2026-01-01T00:00:00Z');
const end   = Cesium.JulianDate.fromIso8601('2026-12-31T23:59:59Z');

<Timeline
  clock={viewer.clock}
  startTime={start}
  endTime={end}
  height={120}
  onTimeChange={(t) => { viewer.clock.currentTime = t; }}
  onPlayPause={(playing) => { viewer.clock.shouldAnimate = playing; }}
  onMultiplierChange={(m) => { viewer.clock.multiplier = m; }}
/>
```

### Restricting Pan/Zoom to a Range

By default the visible window can be panned or zoomed anywhere, regardless of `startTime`/`endTime` — those props only seed the initial view. Set `restrictToRange` to turn them into hard limits: drag, wheel, pinch, edge-scroll, and zoom-to-selection can no longer move the window past them, and playback stops at the boundary instead of running off the edge.

```tsx
const start = Cesium.JulianDate.fromIso8601('2026-01-01T00:00:00Z');
const end   = Cesium.JulianDate.fromIso8601('2026-01-31T23:59:59Z');

<Timeline
  clock={viewer.clock}
  startTime={start}
  endTime={end}
  restrictToRange
  height={120}
  onTimeChange={(t) => { viewer.clock.currentTime = t; }}
/>
```

> `restrictToRange` has no effect unless `startTime` and/or `endTime` are provided — with only one bound set, panning/zooming is limited on that side only.

### Configuring Max Ticks

Useful when the timeline is shown at a small height or in a compact layout.

```tsx
<Timeline
  clock={viewer.clock}
  height={40}
  maxTicks={10}
/>
```

### Configuring Playback Speeds

```tsx
// Slow-motion / real-time / time-lapse only
<Timeline
  clock={viewer.clock}
  ffSpeeds={[0.5, 1, 2, 10, 100]}
  rwSpeeds={[0.5, 1, 2, 10, 100]}
/>
```

### Timeline-Only (No Controls)

```tsx
<Timeline
  clock={viewer.clock}
  height={35}
  showControls={false}
  theme={{ indicatorColor: '#ff6b6b' }}
/>
```

### Custom Date Range

```tsx
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date();
end.setHours(23, 59, 59, 999);

<Timeline
  startTime={start}
  endTime={end}
  height={80}
  dateTimeFormat="HH:mm:ss"
/>
```

### Without Clock (Standalone)

```tsx
import { useState } from 'react';
import * as Cesium from 'cesium';
import { Timeline } from '@kteneyck/cesium-timeline-react';

const StandaloneTimeline = () => {
  const [time, setTime] = useState(new Date());

  return (
    <>
      <p>Selected: {time.toLocaleTimeString()}</p>
      <Timeline
        onTimeChange={(t) => setTime(Cesium.JulianDate.toDate(t))}
        height={80}
        showControls={false}
      />
    </>
  );
};
```

### Zoom to Selection

Click-and-drag in the tick area (the bottom strip, away from the needle) to draw a time-range highlight. The cursor becomes a crosshair while dragging. On mouse-up the **visible window zooms to exactly the selected span** and `onRangeSelect` fires with the start and end `JulianDate`.

A short click (no drag) in the tick area still moves the needle normally.

```tsx
import { useState } from 'react';
import * as Cesium from 'cesium';
import { Timeline } from '@kteneyck/cesium-timeline-react';

const ZoomableTimeline = () => {
  const [selectedRange, setSelectedRange] = useState<{ start: Date; end: Date } | null>(null);

  return (
    <>
      {selectedRange && (
        <p>
          Selected: {selectedRange.start.toISOString()} → {selectedRange.end.toISOString()}
        </p>
      )}
      <Timeline
        clock={viewer.clock}
        height={120}
        onRangeSelect={(start, end) => {
          setSelectedRange({
            start: Cesium.JulianDate.toDate(start),
            end:   Cesium.JulianDate.toDate(end),
          });
        }}
      />
    </>
  );
};
```

In Angular, listen to the `(rangeSelect)` output — the event payload is `{ start: Cesium.JulianDate, end: Cesium.JulianDate }`:

```html
<ct-timeline
  [clock]="viewer.clock"
  [height]="120"
  (rangeSelect)="onRangeSelect($event)"
/>
```

```typescript
onRangeSelect(range: { start: Cesium.JulianDate; end: Cesium.JulianDate }) {
  this.viewer.clock.startTime  = range.start;
  this.viewer.clock.stopTime   = range.end;
  this.viewer.clock.currentTime = range.start;
}
```

---

## Ghost / Preview Needle

When the user hovers over the timeline canvas a **ghost needle** (a semi-transparent version of the current-time indicator) tracks the cursor position in real time. It renders at **15% opacity** using the same `indicatorColor` and `indicatorLineWidth` as the real needle, giving a clear preview of where a click-to-seek or zoom-to-selection drag would begin — without moving the actual current time.

### Behaviour summary

| State | Ghost visible? |
|---|---|
| Mouse idle over canvas | ✅ Follows cursor |
| Dragging (scrub / range-select / slide / zoom) | ❌ Hidden |
| Mouse leaves canvas | ❌ Hidden |

### Theming the ghost needle

The ghost needle inherits `indicatorColor` and `indicatorLineWidth` from your theme — no additional configuration is needed. To change its appearance, update those two theme properties:

```tsx
<Timeline
  clock={viewer.clock}
  theme={{
    indicatorColor: '#ffd54f',
    indicatorLineWidth: 3,
  }}
/>
```

The ghost will render as the same colour at 15% opacity, and the real needle will render at full opacity.

---

## Swim Lanes

Swim lanes render time intervals (bars) and instants (markers) as horizontal rows directly on the timeline canvas, aligned with the ticks and needle. They are ideal for visualizing satellite passes, ground contacts, scheduled events, or any temporal data.

Lanes are rendered in the upper portion of the canvas. The tick area remains fixed at the bottom. As you increase the timeline `height`, more lanes become visible. When lanes overflow the available space, a vertical scrollbar appears and you can scroll with the mouse wheel.

### Basic Swim Lane Example

```tsx
import * as Cesium from 'cesium';
import { Timeline } from '@kteneyck/cesium-timeline-react';
import type { SwimLane } from '@kteneyck/cesium-timeline-react';

const now = Cesium.JulianDate.now();
const later = Cesium.JulianDate.addHours(now, 3, new Cesium.JulianDate());

const swimLanes: SwimLane[] = [
  {
    id: 'passes',
    label: 'Passes',
    items: [
      {
        id: 'pass-1',
        interval: new Cesium.TimeInterval({ start: now, stop: later }),
      },
    ],
  },
];

<Timeline
  clock={viewer.clock}
  height={150}
  swimLanes={swimLanes}
  onTimeChange={(t) => { viewer.clock.currentTime = t; }}
/>
```

### Intervals and Instants

Each `SwimLaneItem` can have an `interval` (rendered as a horizontal bar), an `instant` (rendered as a marker), or both.

```tsx
const lanes: SwimLane[] = [
  {
    id: 'events',
    label: 'Events',
    items: [
      // Interval — rendered as a bar spanning start to stop
      {
        id: 'meeting',
        interval: new Cesium.TimeInterval({
          start: Cesium.JulianDate.fromIso8601('2026-03-05T09:00:00Z'),
          stop:  Cesium.JulianDate.fromIso8601('2026-03-05T10:30:00Z'),
        }),
      },
      // Instant — rendered as a marker at a single point in time
      {
        id: 'alert',
        instant: Cesium.JulianDate.fromIso8601('2026-03-05T12:00:00Z'),
      },
    ],
  },
];
```

### Customizing Styles

Styles cascade: `defaultSwimLaneStyle` → `theme` → `lane.style` → `item.style`. Each level is a partial override.

> **Border shortcut:** The `swimLaneItemBorderColor` and `swimLaneItemBorderWidth` theme properties let you control item borders globally without touching individual lane or item styles. Set `swimLaneItemBorderWidth: 0` in your theme to remove all borders at once.

```tsx
const lanes: SwimLane[] = [
  {
    id: 'maintenance',
    label: 'Maint.',
    // Lane-level style: all items in this lane default to grey
    style: {
      color: '#78909c',
      backgroundColor: 'rgba(120,144,156,0.1)',
    },
    items: [
      {
        id: 'window-1',
        interval: new Cesium.TimeInterval({ start: h(-2), stop: h(0) }),
        // Item-level override: this specific item is red
        style: { color: '#f44336', opacity: 1.0 },
      },
      {
        id: 'window-2',
        interval: new Cesium.TimeInterval({ start: h(4), stop: h(6) }),
        // Inherits lane style (grey)
      },
    ],
  },
];
```

#### `SwimLaneItemStyle` Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `color` | `string` | `#4da6ff` | Fill colour for interval bars and instant markers |
| `borderColor` | `string` | `#2980b9` | Border colour for interval bars |
| `borderWidth` | `number` | `1` | Border width in px for interval bars |
| `opacity` | `number` | `0.8` | Opacity (0–1) |
| `markerShape` | `'diamond' \| 'circle' \| 'line'` | `'diamond'` | Shape used to render instant markers |
| `markerSize` | `number` | `10` | Size in px for instant markers |

#### `SwimLaneStyle` Properties (extends `SwimLaneItemStyle`)

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `labelColor` | `string` | `#cccccc` | Colour of the lane label text |
| `backgroundColor` | `string` | `transparent` | Background colour of the lane row |

### Instant Marker Shapes

Three marker shapes are available for instants:

```tsx
// Diamond (default)
{ id: 'a', instant: someDate, style: { markerShape: 'diamond' } }

// Circle
{ id: 'b', instant: someDate, style: { markerShape: 'circle' } }

// Vertical line
{ id: 'c', instant: someDate, style: { markerShape: 'line' } }
```

### Event Hooks

Swim lane items support click, hover, and double-click events. Each callback receives a `SwimLaneEventInfo` object.

```tsx
import type { SwimLaneEventInfo } from '@kteneyck/cesium-timeline-react';

const handleClick = (info: SwimLaneEventInfo) => {
  console.log(`Clicked item ${info.item.id} in lane ${info.laneId}`);
  console.log('Custom data:', info.item.data);
};

const handleHover = (info: SwimLaneEventInfo | null) => {
  if (info) {
    showTooltip(`${info.item.id} in ${info.laneId}`);
  } else {
    hideTooltip();
  }
};

const handleDoubleClick = (info: SwimLaneEventInfo) => {
  openDetailPanel(info.item.data);
};

<Timeline
  clock={viewer.clock}
  height={150}
  swimLanes={lanes}
  onSwimLaneItemClick={handleClick}
  onSwimLaneItemHover={handleHover}
  onSwimLaneItemDoubleClick={handleDoubleClick}
/>
```

#### `SwimLaneEventInfo`

| Property | Type | Description |
|----------|------|-------------|
| `laneId` | `string` | The `id` of the lane containing the item |
| `item` | `SwimLaneItem` | The item that was interacted with |
| `originalEvent` | `MouseEvent` | The native DOM mouse event |

### Attaching Custom Data

Use the `data` field on `SwimLaneItem` to attach arbitrary metadata. It's passed through in event callbacks.

```tsx
const lanes: SwimLane[] = [
  {
    id: 'contacts',
    label: 'Contacts',
    items: [
      {
        id: 'c-1',
        interval: new Cesium.TimeInterval({ start: t1, stop: t2 }),
        data: { satellite: 'ISS', groundStation: 'Goldstone', snr: 42.5 },
      },
    ],
  },
];

const handleClick = (info: SwimLaneEventInfo) => {
  const { satellite, groundStation, snr } = info.item.data as ContactData;
  console.log(`${satellite} → ${groundStation} (SNR: ${snr})`);
};
```

### Drag-to-Reorder

Pass `onSwimLaneReorder` to enable drag-to-reorder. Grab a lane by its label (left side) and drag up or down. An insertion indicator shows where the lane will be placed.

```tsx
const [lanes, setLanes] = useState<SwimLane[]>(initialLanes);

const handleReorder = (orderedIds: string[]) => {
  // orderedIds is the new order of lane IDs
  setLanes(prev => orderedIds.map(id => prev.find(l => l.id === id)!));
};

<Timeline
  clock={viewer.clock}
  height={150}
  swimLanes={lanes}
  onSwimLaneReorder={handleReorder}
/>
```

### Show / Hide Swim Lanes

Toggle visibility without removing the data:

```tsx
const [showSwimLanes, setShowSwimLanes] = useState(true);

<Timeline
  clock={viewer.clock}
  height={150}
  swimLanes={lanes}
  showSwimLanes={showSwimLanes}
/>

<button onClick={() => setShowSwimLanes(v => !v)}>
  {showSwimLanes ? 'Hide' : 'Show'} Swim Lanes
</button>
```

When hidden, the full canvas height is used for the tick/label area as normal.

### Lane Height and Scrolling

Each lane defaults to 24px tall. Override per-lane with the `height` property:

```tsx
const lanes: SwimLane[] = [
  { id: 'big', label: 'Important', height: 40, items: [...] },
  { id: 'small', label: 'Minor', height: 16, items: [...] },
];
```

When the total lane height exceeds the available space (canvas height minus the 36px tick area), a scrollbar appears and the mouse wheel scrolls vertically in the swim lane region. Outside the lane region, the mouse wheel zooms the timeline as usual.

### Imperative API

The `TimelineCanvasHandle` (accessible via a ref on `Timeline`) exposes methods for programmatic swim lane management:

```tsx
const timelineRef = useRef<TimelineCanvasHandle>(null);

// Append a new lane
timelineRef.current?.appendSwimLane(newLane);

// Update an existing lane
timelineRef.current?.updateSwimLane('lane-id', updatedLane);

// Remove a lane
timelineRef.current?.removeSwimLane('lane-id');

// Reorder lanes
timelineRef.current?.reorderSwimLanes(['lane-b', 'lane-a', 'lane-c']);
```

---

## Building

```bash
cd cesium-timeline
npm install

# Build all packages (core → react → angular)
npm run build

# Build individually
npm run build:core
npm run build:react
npm run build:angular

# Development
npm run dev:demo     # React demo with hot reload
npm run typecheck    # TypeScript check (core + react)
npm run clean        # Remove all build artifacts
```

### Project Structure

```
cesium-timeline/
├── packages/
│   ├── core/       → @kteneyck/cesium-timeline-core     (types, utils, canvas engine)
│   ├── react/      → @kteneyck/cesium-timeline-react    (React components)
│   └── angular/    → @kteneyck/cesium-timeline-angular  (Angular standalone components)
├── demo-react/     → React demo app (npm run dev:demo)
├── src/            → Original source (preserved, not used by packages)
└── package.json    → npm workspace root
```

---

## License

MIT

