{"_id":"@atelier83/timeline","_rev":"2-023399e936a5f64ce91671fbaccf642c","name":"@atelier83/timeline","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@atelier83/timeline","version":"0.1.0","keywords":["timeline","animation","keyframes","easing","tween","dope-sheet","playback","headless","react"],"author":{"name":"atelier83"},"license":"MIT","_id":"@atelier83/timeline@0.1.0","maintainers":[{"name":"andrevenancio","email":"info@andrevenancio.com"}],"homepage":"https://github.com/atelier83/timeline#readme","bugs":{"url":"https://github.com/atelier83/timeline/issues"},"dist":{"shasum":"43cb9926e3b6484cb62a3343481f4cb4a75df55d","tarball":"https://registry.npmjs.org/@atelier83/timeline/-/timeline-0.1.0.tgz","fileCount":37,"integrity":"sha512-YaIJBVocYX8VpxT6gAmYVUqc1oMjwdIQByaOY9Dwdnj0bG1ID+0Emp9lr3mZipx3n1keydDMvutIA+Wot0YCVQ==","signatures":[{"sig":"MEYCIQDbzkJy2MdVJuCR1JBSlFw5tvuWdAahV6uKZFc2MO2VdwIhANeMkD98WYOam+Xa0SCGZ+6r+HNA2q/hsoXJc8K308vF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":171101},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./ui":{"types":"./dist/ui/index.d.ts","import":"./dist/ui/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js"},"./theme.css":"./dist/theme.css"},"gitHead":"d7ff1179a032872a88dfd080e7186e18a93e5168","scripts":{"dev":"vite","test":"vitest run","build":"tsc -p tsconfig.build.json && cp src/theme.css dist/theme.css","build:demo":"vite build","test:watch":"vitest","check-types":"tsc --noEmit","test:coverage":"vitest run --coverage","prepublishOnly":"pnpm test && pnpm build"},"_npmUser":{"name":"andrevenancio","email":"info@andrevenancio.com"},"repository":{"url":"git+https://github.com/atelier83/timeline.git","type":"git"},"_npmVersion":"10.9.3","description":"Headless, framework-agnostic animation timeline: keyframes, easing, and playback. Vanilla TS core with an optional Flash-style dope-sheet UI and React bindings.","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"22.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"vite":"^6.0.0","jsdom":"^29.1.1","react":"^19.0.0","vitest":"^3.0.0","react-dom":"^19.0.0","typescript":"^5.6.0","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","@vitest/coverage-v8":"^3.2.6","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2"},"peerDependencies":{"react":">=18","react-dom":">=18"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/timeline_0.1.0_1781144123938_0.058863838366747245","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@atelier83/timeline","version":"0.2.0","description":"Headless, framework-agnostic animation timeline: keyframes, easing, and playback. Vanilla TS core with an optional Flash-style dope-sheet UI and React bindings.","license":"MIT","author":{"name":"atelier83"},"homepage":"https://github.com/atelier83/timeline#readme","repository":{"type":"git","url":"git+https://github.com/atelier83/timeline.git"},"bugs":{"url":"https://github.com/atelier83/timeline/issues"},"keywords":["timeline","animation","keyframes","easing","tween","dope-sheet","playback","headless","react"],"type":"module","packageManager":"pnpm@10.33.0","publishConfig":{"access":"public"},"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./ui":{"types":"./dist/ui/index.d.ts","import":"./dist/ui/index.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js"},"./theme.css":"./dist/theme.css"},"sideEffects":["**/*.css"],"scripts":{"dev":"vite","build":"tsc -p tsconfig.build.json && cp src/theme.css dist/theme.css","build:demo":"vite build","check-types":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","changeset":"changeset","version":"changeset version","release":"pnpm test && pnpm build && pnpm publish","prepublishOnly":"pnpm test && pnpm build"},"peerDependencies":{"react":">=18","react-dom":">=18"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"devDependencies":{"@changesets/cli":"^2.31.0","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.2","@types/react":"^19.0.0","@types/react-dom":"^19.0.0","@vitest/coverage-v8":"^3.2.6","jsdom":"^29.1.1","react":"^19.0.0","react-dom":"^19.0.0","typescript":"^5.6.0","vite":"^6.0.0","vitest":"^3.0.0"},"engines":{"node":">=20"},"_id":"@atelier83/timeline@0.2.0","gitHead":"86c23a31b6fcf81e64c0cb53ab0eb75c05ff774a","_nodeVersion":"22.19.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-QTo1lkVd0Lpx0B7D2R65jaP/mUu2S7acrWOtRwJgQzAGsriVWxv9wOnJ22J2oG2s4YHGiL4oCQQf6z6IB66VIA==","shasum":"eeb39eff67a3276894395e745f2d1f3d41ebe255","tarball":"https://registry.npmjs.org/@atelier83/timeline/-/timeline-0.2.0.tgz","fileCount":37,"unpackedSize":172178,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDlJGiMohp5smog/gCUzEGHKUenygkmIUZz6vy/I9di6gIgX1FaDVta3LJDX0FfLfz+Y6FldyZT31ZgRNGcKOJQoH4="}]},"_npmUser":{"name":"andrevenancio","email":"info@andrevenancio.com"},"directories":{},"maintainers":[{"name":"andrevenancio","email":"info@andrevenancio.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/timeline_0.2.0_1781229071269_0.26676015922358864"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-11T02:15:23.745Z","modified":"2026-06-12T01:51:11.594Z","0.1.0":"2026-06-11T02:15:24.069Z","0.2.0":"2026-06-12T01:51:11.450Z"},"bugs":{"url":"https://github.com/atelier83/timeline/issues"},"author":{"name":"atelier83"},"license":"MIT","homepage":"https://github.com/atelier83/timeline#readme","keywords":["timeline","animation","keyframes","easing","tween","dope-sheet","playback","headless","react"],"repository":{"type":"git","url":"git+https://github.com/atelier83/timeline.git"},"description":"Headless, framework-agnostic animation timeline: keyframes, easing, and playback. Vanilla TS core with an optional Flash-style dope-sheet UI and React bindings.","maintainers":[{"name":"andrevenancio","email":"info@andrevenancio.com"}],"readme":"# @atelier83/timeline\n\n> Headless, framework-agnostic animation timeline for the web — keyframes, easing, and playback, with an optional Flash-style dope-sheet UI.\n\n[![npm](https://img.shields.io/npm/v/@atelier83/timeline.svg)](https://www.npmjs.com/package/@atelier83/timeline)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@atelier83/timeline.svg)](https://bundlephobia.com/package/@atelier83/timeline)\n[![CI](https://github.com/atelier83/timeline/actions/workflows/ci.yml/badge.svg)](https://github.com/atelier83/timeline/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@atelier83/timeline.svg)](./LICENSE)\n\nThe kind of timeline you get in Flash/Animate or a motion tool. You bind numeric properties of any object to keyframes; the engine evaluates them over time, writes the values back, and drives its own playback. The core renders nothing — it's a small state machine you can wrap in any framework. When you want to author by hand, opt into the bundled dope-sheet UI: a bottom-of-screen panel with lanes, draggable keyframes, easing per segment, scrubbing, and labels.\n\n## Features\n\n- **Headless core** — vanilla TypeScript. No DOM, no rendering, no dependencies.\n- **Keyframes + easing** — per-segment curves (linear, quad, cubic) or stepped holds.\n- **Frame-accurate** — author in frames at any fps; the engine keeps time in seconds.\n- **Auto-fitting length** — the duration grows and shrinks to the furthest content.\n- **Self-driving or app-driven** — plays on its own rAF loop, or hand it your loop with `update(dt)`.\n- **Optional dope-sheet UI** — a token-themed panel you can drop in.\n- **Optional React bindings** — a thin `useTimeline` hook and `<TimelineDock>`.\n\n## Contents\n\n- [Install](#install)\n- [Why headless?](#why-headless)\n- [Concepts](#concepts)\n- [Quick start](#quick-start)\n- [Driving playback](#driving-playback)\n- [The dope-sheet UI](#the-dope-sheet-ui)\n- [React](#react)\n- [Easing](#easing)\n- [Styling](#styling)\n- [API reference](#api-reference)\n- [When not to use this](#when-not-to-use-this)\n- [Development](#development)\n- [License](#license)\n\n## Install\n\n```bash\nnpm install @atelier83/timeline\n# or: pnpm add @atelier83/timeline\n```\n\n`react` and `react-dom` (>=18) are optional peer dependencies, only needed if you use `@atelier83/timeline/react`.\n\n## Why headless?\n\nA timeline is really two things: a small engine that maps time to values, and a UI for editing it. They usually ship welded together, so the editor is the thing you fight when it doesn't fit your tool. `timeline` splits them. The core (`Timeline`, `Track`) is pure logic — no DOM, no styling, no framework — so you can run it in a game loop, a worker, an SSR build, or behind your own UI. The dope-sheet UI is a separate, optional layer that talks to the same engine through plain events.\n\n## Concepts\n\n- **Timeline**: owns the tracks, the playhead, and playback. Time is in seconds; you author in frames.\n- **Track**: binds one numeric property of a target object to a list of keyframes. `evaluate(time)` returns the interpolated value; `apply(time)` writes it back.\n- **Keyframe**: `{ time, value, easing }`. The `easing` is applied to the segment _leaving_ the keyframe; `\"none\"` means a stepped hold.\n\n## Quick start\n\n```ts\nimport { Timeline } from \"@atelier83/timeline\";\n\nconst state = { x: 0, opacity: 1 };\n\nconst tl = new Timeline({ fps: 30, loop: true });\n\ntl.add(state, \"x\", { min: 0, max: 100 })\n  .addKeyframe(0, 0, \"easeInOutCubic\")\n  .addKeyframe(30, 100, \"easeInOutCubic\")\n  .addKeyframe(60, 0);\n\ntl.add(state, \"opacity\", { min: 0, max: 1 })\n  .addKeyframe(0, 1)\n  .addKeyframe(60, 0, \"linear\");\n\ntl.on(\"update\", () => {\n  // state.x / state.opacity now hold the values for the current frame\n  render(state);\n});\n\ntl.play();\n```\n\nThe timeline runs its own `requestAnimationFrame` loop while playing, so it works standalone in any page. Don't have a render hook? Read values off your target object whenever you like, or call `tl.seek(time)` to jump and evaluate without playing.\n\n## Driving playback\n\nLike `dat.gui`/`lil-gui`, a `Timeline` mutates the properties of a plain object — `tl.add(target, \"prop\", { min, max })` binds one numeric field, and every frame the engine writes the interpolated value back into `target`. How that frame is pumped is up to you:\n\n**Self-driving (default).** Call `play()` and the timeline runs its own `requestAnimationFrame` loop, mutating the target until you `pause()`/`stop()` — nothing else required:\n\n```ts\nconst tl = new Timeline({ loop: true });\ntl.add(state, \"x\", { min: 0, max: 100 }).addKeyframe(0, 0).addKeyframe(60, 100);\ntl.play(); // self-driven; state.x updates on its own\n```\n\n**App-driven.** When you already own a render loop (a game loop, a WebGL/Three.js `requestAnimationFrame`, a worker tick), construct with `autoUpdate: false`. Now `play()` only flips the playing flag and you advance the playhead by calling `update()` once per frame:\n\n```ts\nconst tl = new Timeline({ loop: true, autoUpdate: false });\ntl.add(state, \"x\", { min: 0, max: 100 }).addKeyframe(0, 0).addKeyframe(60, 100);\n\nfunction frame() {\n  tl.update(); // advances while playing, no-op while paused\n  renderMyScene(state); // state.x is up to date for this frame\n  requestAnimationFrame(frame);\n}\n\ntl.play();\nrequestAnimationFrame(frame);\n```\n\n`update()` derives the delta from its own clock, so a bare call works in any loop. If you already track a per-frame delta, pass it explicitly — `update(dtSeconds)` — to stay perfectly in sync with the rest of your loop (and to honour the loop's own pause/slow-mo). `update()` is a no-op while paused, so it's safe to call unconditionally.\n\n## The dope-sheet UI\n\nPull in the UI when you want to author keyframes by hand. `createTimeline` builds a `Timeline`, mounts a bottom-of-screen panel, and returns the timeline (with a `.ui` handle):\n\n```ts\nimport { createTimeline } from \"@atelier83/timeline/ui\";\n\nconst tl = createTimeline({ fps: 30, loop: true });\n\ntl.add(settings, \"rotation\", { min: -180, max: 180 })\n  .addKeyframe(0, 0, \"easeInOutCubic\")\n  .addKeyframe(30, 180, \"easeInOutCubic\");\n```\n\nThe panel is a Flash-style dope sheet:\n\n- One **lane per track**; the sidebar lists the track labels (click to make a track active).\n- **Keyframes** are dots; the **segment** between two is drawn as a block — lighter for a tween, darker for a stepped hold.\n- The **Insert keyframe** button drops a key at the playhead on the active track using the property's current value; **Insert frame** extends the held tail.\n- Select a keyframe to edit its **frame / value / easing** in the inline inspector, or drag segments to retime them; drag a segment's right edge to change its duration (later keys ripple along).\n- **Scrub** by dragging the ruler; **Space** toggles playback; **Delete** removes the selection.\n\nYou can also drive the engine and the UI yourself:\n\n```ts\nimport { Timeline } from \"@atelier83/timeline\";\nimport { TimelineUI } from \"@atelier83/timeline/ui\";\n\nconst tl = new Timeline({ fps: 24 });\nconst ui = new TimelineUI(tl, { pixelsPerFrame: 14 });\nui.mount(document.getElementById(\"editor\")!); // defaults to document.body\n```\n\n## React\n\n```tsx\nimport { useEffect } from \"react\";\nimport { useTimeline } from \"@atelier83/timeline/react\";\n\nfunction Scene({ state }: { state: { x: number } }) {\n  const tl = useTimeline({ fps: 30, loop: true });\n\n  useEffect(() => {\n    const track = tl.add(state, \"x\", { min: 0, max: 100 });\n    track.addKeyframe(0, 0).addKeyframe(30, 100, \"easeInOutCubic\");\n    return () => tl.remove(track);\n  }, [tl]);\n\n  // ...\n}\n```\n\n`useTimeline` returns a timeline that's stable across renders and mounts the dope-sheet UI by default (pass `ui: false` to skip it). For a declarative entry point, `<TimelineDock onReady={(tl) => …} />` creates the timeline, mounts the UI, and hands you the instance once.\n\n## Easing\n\nEach keyframe carries the easing for its _outgoing_ segment. Built-ins: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, and the special `\"none\"` (a stepped hold — the value stays put until the next keyframe).\n\n```ts\nimport { easings, interpolate, easingOptions } from \"@atelier83/timeline\";\n\ninterpolate(0, 100, 0.5, \"easeInOutCubic\"); // -> 50\neasings.easeInQuad(0.5); // -> 0.25 (raw t-mapping)\neasingOptions; // [{ value, label }] for building a <select>\n```\n\n## Styling\n\nThe dope-sheet UI injects its structural rules on mount, but it carries **no colours of its own** — every value is read from a shared `@atelier83` design token (`--a83-*`). A theme defines those tokens; the library only consumes them. You have two ways to provide one.\n\n### Option 1: the bundled dark theme\n\nLoad the default palette — a flat, dark grey look:\n\n```ts\nimport \"@atelier83/timeline/theme.css\";\n```\n\nWithout it (and without app-defined tokens), the `--a83-*` tokens are undefined and the panel renders unstyled.\n\n### Option 2: define the tokens yourself\n\n`@atelier83/timeline` and [`@atelier83/layouts`](https://www.npmjs.com/package/@atelier83/layouts) read the **same** `--a83-*` tokens, so defining them once themes both packages together — they share one palette by design. Skip the bundled CSS and set the tokens on `:root` (or any ancestor):\n\n```css\n:root {\n  --a83-surface: #323232; /* panels / toolbars / lanes */\n  --a83-text: #c8c8c8;\n  --a83-text-muted: #8c8c8c;\n  --a83-border: #212121;\n  --a83-accent: #e6e6e6; /* playhead, focus */\n  /* …the rest of the palette */\n}\n```\n\nTokens: `--a83-bg`, `--a83-surface`, `--a83-border`, `--a83-border-strong`, `--a83-text`, `--a83-text-muted`, `--a83-accent`, `--a83-control`, `--a83-control-hover`, `--a83-hover`, `--a83-active`, `--a83-overlay`, `--a83-highlight`, `--a83-font`, `--a83-radius-sm`, `--a83-radius-md`, `--a83-radius-lg`. This is the exact same set `@atelier83/layouts` uses — there are no timeline-specific variables; the dope-sheet spans and keyframe markers are derived from `--a83-text`, `--a83-accent`, and `--a83-bg` (the lane canvas).\n\nThere are no fallbacks and no built-in light/dark switching: the library always reads `var(--a83-*)`, and the theme decides what those resolve to. **Light/dark/system is your app's job** — redefine the tokens under your own `prefers-color-scheme` media query or `[data-theme]` rules. See [`theme.css`](./src/theme.css) for the full default set.\n\n## API reference\n\n### `new Timeline(options?)`\n\n`options`: `{ fps?, loop?, speed?, minFrames?, autoUpdate?, onUpdate? }`. Set `autoUpdate: false` to drive playback from your own loop via `update(dt)` (see [Driving playback](#driving-playback)).\n\n| member                                     | description                                                                               |\n| ------------------------------------------ | ----------------------------------------------------------------------------------------- |\n| `add(target, prop, options?)`              | bind a numeric property; returns the `Track`                                              |\n| `remove(track)`                            | detach a track                                                                            |\n| `seek(time)`                               | move the playhead (seconds), evaluate, and emit `seek`/`update`                           |\n| `play(dir?)`                               | begin playback (`+1` forward, `-1` backward); runs an rAF loop unless `autoUpdate: false` |\n| `pause()` / `stop()` / `toggle()`          | stop (and, for `stop`, rewind to 0)                                                       |\n| `update(dt?)`                              | advance playback by `dt` seconds (or an auto-derived delta); for app-driven loops         |\n| `apply()`                                  | re-evaluate every track at the current time and emit `update`                             |\n| `frameToTime` / `timeToFrame` / `snapTime` | frame ↔ second helpers                                                                    |\n| `on(event, fn)` / `off(event, fn)`         | subscribe to events; `on` returns an unsubscribe function                                 |\n| `dispose()`                                | pause and clear tracks and listeners                                                      |\n\nProperties: `fps`, `loop`, `speed`, `autoUpdate`, `tracks`, `currentTime`, `currentFrame`, `isPlaying`, `direction`, `duration`, `totalFrames`.\n\nEvents: `update` (time), `change`, `keyframes` (track), `play` (direction), `pause`, `stop`, `seek` (time).\n\n### `Track`\n\n| member                                   | description                                                 |\n| ---------------------------------------- | ----------------------------------------------------------- |\n| `addKeyframe(frame, value, easing?)`     | add a keyframe (chainable); value is clamped to `min`/`max` |\n| `moveKeyframe(id, patch)`                | retime / revalue / re-ease a keyframe                       |\n| `removeKeyframe(id)` / `getKeyframe(id)` | edit and read keyframes                                     |\n| `setSpanEnd(time)`                       | extend a held tail past the last keyframe                   |\n| `evaluate(time)`                         | the value at `time` (or `undefined` when unkeyed)           |\n| `apply(time)`                            | evaluate and write into the target                          |\n| `getCurrentValue()`                      | the target's live value                                     |\n| `endTime` / `hasKeyframes()`             | derived state                                               |\n| `lastKeyframe`                           | the most recently added keyframe (or `null`)                |\n\n### UI (`@atelier83/timeline/ui`)\n\n| export                       | description                                                                                    |\n| ---------------------------- | ---------------------------------------------------------------------------------------------- |\n| `createTimeline(options?)`   | build a timeline + dope-sheet UI and mount it (returns the timeline with a `.ui` handle)       |\n| `TimelineUI`                 | the dope-sheet view; `new TimelineUI(timeline, options?)`, then `mount(parent?)` / `dispose()` |\n| `injectStyles(doc?)` / `CSS` | inject (or read) the bundled stylesheet                                                        |\n\n`createTimeline`/`TimelineUI` options: `pixelsPerFrame`, `collapsed` (and, for `createTimeline`, `autoMount` and `parent` plus all `Timeline` options).\n\n### React (`@atelier83/timeline/react`)\n\n| export                  | description                                                                |\n| ----------------------- | -------------------------------------------------------------------------- |\n| `useTimeline(options?)` | create a render-stable timeline; mounts the UI unless `ui: false`          |\n| `<TimelineDock>`        | headless component that creates the timeline and calls `onReady(timeline)` |\n\n## When not to use this\n\n`timeline` is a small, unopinionated engine. Reach for something else if you need:\n\n- a general-purpose tweening/animation library for one-off transitions — [GSAP](https://gsap.com/) or [motion](https://motion.dev/) fit better;\n- a full NLE / video editor timeline with clips, tracks of media, and trimming;\n- spring physics rather than keyframed curves — try [react-spring](https://www.react-spring.dev/).\n\nIt's a good fit when you want frame-based keyframe authoring, a vanilla core you can wrap in any framework, and an optional editor you fully control the look of.\n\n## Development\n\n```bash\npnpm install\npnpm dev          # live playground (DOM boxes driven by the timeline) at http://localhost:5173\npnpm test         # run the test suite once\npnpm test:watch   # watch mode\npnpm check-types  # type-check without emitting\npnpm build        # build the library to dist/\npnpm build:demo   # bundle the playground into demo-dist/ for hosting\n```\n\nThe `playground/` page imports the library source directly and animates a row of plain DOM boxes by keyframing their CSS transforms. `pnpm build:demo` bundles it into `demo-dist/`, which you can deploy to any static host.\n\n## License\n\n[MIT](./LICENSE) © atelier83\n","readmeFilename":"README.md"}