{"_id":"@boflyeta/motio","_rev":"2-e2e6316776c8a0aa82a422bba958fae9","name":"@boflyeta/motio","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@boflyeta/motio","version":"0.1.0","keywords":["animation","tween","easing","flip","spring","requestanimationframe","zero-dependency"],"author":{"name":"Bofly Eta"},"license":"MIT","_id":"@boflyeta/motio@0.1.0","maintainers":[{"name":"boflyeta","email":"boflyetadev@gmail.com"}],"homepage":"https://boflyeta.github.io/Motio/demo/","bugs":{"url":"https://github.com/BoflyEta/Motio/issues"},"dist":{"shasum":"cd493c0164c3a2c54bbf36f5ccdedefb65aa7e5e","tarball":"https://registry.npmjs.org/@boflyeta/motio/-/motio-0.1.0.tgz","fileCount":67,"integrity":"sha512-lxlnai2PQiYGUd/QAlfdFfLhL5ltnHhTZF4qGiO/wz37CQrIdD/vD98WofkyikVghckM9Qr5enJXU/rQoEK33g==","signatures":[{"sig":"MEUCIDWWWw84VxmFi5ZBCECi/ublm51c0WocZnawiy701fdPAiEAi/l8sUNuyQVZU/dDMqP2WPPeVBl70MOAf09vjOPRE+A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222673},"main":"./src/index.js","type":"module","types":"./dist/index.d.ts","module":"./src/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./src/index.js"},"./package.json":"./package.json"},"gitHead":"4ac579fd591c5db29e2a7da444fa619962d25c95","scripts":{"demo":"npx --yes serve -l 5173 .","lint":"tsc -p tsconfig.lint.json","test":"vitest run","build":"npm run types && npm run bundle","types":"tsc -p tsconfig.json","bundle":"esbuild src/index.js --bundle --format=esm --minify --outfile=dist/motio.min.js","prepack":"npm run build","test:watch":"vitest","prepublishOnly":"npm run lint && npm test"},"_npmUser":{"name":"boflyeta","email":"boflyetadev@gmail.com"},"repository":{"url":"git+https://github.com/BoflyEta/Motio.git","type":"git"},"_npmVersion":"11.17.0","description":"Zero-dependency JavaScript animation library. One shared rAF ticker, transform/opacity-only presets, FLIP layout animation, and first-class reduced-motion support.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.2.0","esbuild":"^0.25.0","typescript":"^5.8.0"},"_npmOperationalInternal":{"tmp":"tmp/motio_0.1.0_1787762567413_0.6467516774488014","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@boflyeta/motio","version":"0.1.2","description":"Zero-dependency JavaScript animation library. One shared rAF ticker, transform/opacity-only presets, FLIP layout animation, and first-class reduced-motion support.","type":"module","sideEffects":false,"license":"MIT","publishConfig":{"access":"public"},"keywords":["animation","tween","easing","flip","spring","requestanimationframe","zero-dependency"],"engines":{"node":">=18"},"main":"./src/index.js","module":"./src/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./src/index.js"},"./package.json":"./package.json"},"scripts":{"test":"vitest run","test:watch":"vitest","lint":"tsc -p tsconfig.lint.json","types":"tsc -p tsconfig.json","bundle":"esbuild src/index.js --bundle --format=esm --minify --outfile=dist/motio.min.js","build":"npm run types && npm run bundle","demo":"npx --yes serve -l 5173 .","prepublishOnly":"npm run lint && npm test","prepack":"npm run build"},"devDependencies":{"esbuild":"^0.25.0","typescript":"^5.8.0","vitest":"^3.2.0"},"author":{"name":"Bofly Eta"},"repository":{"type":"git","url":"git+https://github.com/BoflyEta/Motio.git"},"homepage":"https://boflyeta.github.io/Motio/demo/","bugs":{"url":"https://github.com/BoflyEta/Motio/issues"},"gitHead":"134fb8e395ebfbaa83b95556ea751d4331e931c1","_id":"@boflyeta/motio@0.1.2","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-qplC5AzlVQNDBGIWaKPIo3jPXNKowdT2/Wf4dSD6J80wAWf2ayUln2PtbFy5SHBAEIJ4SV+Ag/EV6JPfkgNoiw==","shasum":"3f9dbde644b1506b9ce91195f3d4124f3e2b8a1c","tarball":"https://registry.npmjs.org/@boflyeta/motio/-/motio-0.1.2.tgz","fileCount":70,"unpackedSize":258864,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHH5NW4BWoQ0IBgb5suP8sASwh/6CBzMPHeCSSJ+u+g0AiB+mm/VNZFR1LepNRmDVP/5dBDtUSSwvdqigmVVt4wONg=="}]},"_npmUser":{"name":"boflyeta","email":"boflyetadev@gmail.com"},"directories":{},"maintainers":[{"name":"boflyeta","email":"boflyetadev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/motio_0.1.2_1788014028254_0.8292492837501544"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-26T16:42:47.181Z","modified":"2026-08-29T14:33:48.596Z","0.1.0":"2026-08-26T16:42:47.610Z","0.1.2":"2026-08-29T14:33:48.422Z"},"bugs":{"url":"https://github.com/BoflyEta/Motio/issues"},"author":{"name":"Bofly Eta"},"license":"MIT","homepage":"https://boflyeta.github.io/Motio/demo/","keywords":["animation","tween","easing","flip","spring","requestanimationframe","zero-dependency"],"repository":{"type":"git","url":"git+https://github.com/BoflyEta/Motio.git"},"description":"Zero-dependency JavaScript animation library. One shared rAF ticker, transform/opacity-only presets, FLIP layout animation, and first-class reduced-motion support.","maintainers":[{"name":"boflyeta","email":"boflyetadev@gmail.com"}],"readme":"# motio — a zero-dependency JavaScript animation library\n\n<!-- Record a screen capture of the demo gallery and drop it in as docs/demo.gif -->\n![motio demo](docs/demo.gif)\n\n[![Live demo](https://img.shields.io/badge/live-demo-6ee7b7?style=flat-square)](https://boflyeta.github.io/Motio/demo/)\n[![npm](https://img.shields.io/npm/v/%40boflyeta%2Fmotio?style=flat-square)](https://www.npmjs.com/package/@boflyeta/motio)\n[![CI](https://github.com/BoflyEta/Motio/actions/workflows/ci.yml/badge.svg)](https://github.com/BoflyEta/Motio/actions/workflows/ci.yml)\n[![dependencies](https://img.shields.io/badge/dependencies-0-6ee7b7?style=flat-square)](package.json)\n[![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)\n\n**Zero runtime dependencies.** One shared `requestAnimationFrame` loop, presets that touch only\n`transform` and `opacity`, FLIP for layout changes, interruptions that carry an element's real\nvelocity into the animation that replaces them, and reduced motion handled rather than bolted on.\n10.7 kB gzipped for all of it; 5.6 kB if you import three presets, because it tree-shakes.\n\n## Why I built this\n\nMost animation libraries are either a 40 kB timeline engine or a one-file tweener that spawns a\n`requestAnimationFrame` loop per animation and animates whatever property you name. I wanted to\nfind out what the middle actually costs to build: a shared scheduler, a DOM-agnostic interpolation\nprimitive, and presets constrained to the two properties the compositor can animate without\ntouching layout. The constraints turned out to be the interesting part — most of the design below\nfalls out of them.\n\n## Quick start\n\n```bash\nnpm install @boflyeta/motio\n```\n\n```js\nimport { fadeIn, timeline, slideIn } from '@boflyeta/motio';\n\nfadeIn('.card', { stagger: 80 });\n```\n\nNo build step is required to use it. The package ships ES modules and the demo in this repo\nimports `src/index.js` directly from a static file server.\n\n## Examples\n\n**fadeIn / fadeOut** — opacity only, staggered across a set.\n\n```js\nfadeIn('.card', { stagger: 80, duration: 520 });\nfadeOut('.toast', { duration: 300 });\n```\n\n**slideIn** — translate plus fade, from any of four directions. `direction` names where the\nelement travels *to*, so `'up'` starts below its resting position.\n\n```js\nslideIn('.card', { direction: 'up', distance: 32, stagger: 60 });\n```\n\n**scaleIn** — scale and fade together.\n\n```js\nscaleIn('.modal', { from: 0.9, easing: 'backOut' });\n```\n\n**spring** — a real damped oscillator. There is no `duration` option; it comes out of the physics\nand is readable afterwards.\n\n```js\nconst controls = spring('.badge', {\n  from: { scale: 0.4, y: 20 },\n  stiffness: 220,\n  damping: 12,\n});\n\ncontrols.duration; // e.g. 812 — derived, not chosen\n```\n\n**interruption** — `from: 'current'` starts from wherever the element is, and `velocity: 'inherit'`\nstarts at whatever speed it is already moving. Grab a card mid-slide and it redirects instead of\nstopping dead for a frame.\n\n```js\nslideIn('.card', { direction: 'up', distance: 120, duration: 800 });\n\ncard.addEventListener('pointerdown', () => {\n  spring('.card', { from: 'current', to: { y: 0 }, velocity: 'inherit' });\n});\n```\n\n`velocityOf(el, 'y')` reports the same measurement directly, in pixels per second.\n\n**flipList** — animate a reorder, filter, or insertion using only transforms.\n\n```js\nflipList('.list li', {\n  mutate: () => list.append(...shuffle([...list.children])),\n  duration: 450,\n  easing: 'quartOut',\n});\n```\n\n**drawSVG** — animate `stroke-dashoffset` so the geometry never changes.\n\n```js\ndrawSVG('#signature path', { duration: 1200, stagger: 120 });\n```\n\n**scrollScrub** — bind an element's position in the viewport to a tween's `seek`.\n\n```js\nscrollScrub('.parallax', { from: { y: 60 }, to: { y: -60 } });\n```\n\n**textScramble** — decode text from noise, with the real string on `aria-label` while it settles.\n\n```js\ntextScramble('.status', { text: 'Connected', duration: 900 });\n```\n\n**splitText** — per-character reveal. Generated spans are `aria-hidden`; the sentence stays on the\ncontainer.\n\n```js\nsplitText('.headline', { stagger: 24, y: 18, easing: 'quartOut' });\n```\n\n**magneticHover** — pull an element toward the pointer, ease it back on leave.\n\n```js\nconst magnet = magneticHover('.cta', { strength: 0.4, scale: 1.06 });\nmagnet.cancel(); // unbinds and resets\n```\n\n**particleBurst** — canvas confetti from an element or a point.\n\n```js\nbutton.addEventListener('click', () => particleBurst(button, { count: 80 }));\n```\n\n**counter** — an animated number that lands on an exact value.\n\n```js\ncounter('.stat', { to: 12480, duration: 1400, easing: 'expoOut' });\ncounter('.price', { to: 49.99, decimals: 2, prefix: '$' });\n```\n\n**timeline** — sequencing with relative offsets and stagger.\n\n```js\ntimeline()\n  .add('.hero h1', slideIn, { direction: 'up', duration: 500 })\n  .add('.hero p', fadeIn, { at: '-=300' })\n  .add('.card', scaleIn, { stagger: 60, at: '-=200' });\n```\n\n**tween** — the primitive underneath all of it, with no DOM knowledge at all.\n\n```js\nconst controls = tween({\n  from: { x: 0, scale: 0.8 },\n  to: { x: 120, scale: 1 },\n  easing: 'backOut',\n  onUpdate: ({ x, scale }) => {\n    el.style.transform = `translateX(${x}px) scale(${scale})`;\n  },\n});\n\nawait controls.finished;\n```\n\n## How it works\n\n### One loop, not one per animation\n\nEvery running animation in motio is a subscriber to a single `requestAnimationFrame` loop. The\nobvious alternative — each animation calling `requestAnimationFrame` for itself — works fine for\none animation and gets steadily worse as you add more, in ways that are easy to miss until a page\nis busy.\n\nThe first problem is time. Each independent loop computes its delta from its own start, so two\nanimations that are supposed to be in lockstep drift apart by a fraction of a frame and stay\ndrifted. A staggered list animated by twenty separate loops is twenty slightly different\ninterpretations of \"now\"; driven by one loop, every subscriber receives the identical timestamp\nfor a frame, and a stagger is exact by construction. The second problem is layout. If each\nanimation independently reads geometry and then writes styles, the frame becomes a sequence of\nread-write-read-write, and every read after a write forces the browser to flush pending layout to\nanswer honestly. One loop makes it possible to order that work; twenty loops make it impossible to\neven see. The third is that a loop nobody is using still costs something — a loop that keeps\nscheduling frames while nothing is animating quietly prevents the browser from going idle, which\non a laptop is battery. motio's ticker stops scheduling entirely when the last subscriber leaves\nand restarts on the next subscribe, and `activeCount()` is exported so you can assert that a page\nis genuinely idle rather than hoping.\n\nTwo implementation details are worth naming because both caused real bugs during development.\nSubscriptions that arrive *during* a tick are queued and flushed before and after the iteration\nrather than applied immediately: mutating the subscriber set mid-iteration is not a crash, but its\nsemantics are the wrong ones, since an entry added during a `for...of` is visited in that same\npass. A tween that completes and starts a follow-up would otherwise run the follow-up's first\nframe with the finishing tween's delta, and a handler that resubscribes itself would spin forever.\nThe subtler one: because a tick clears its scheduled-frame handle before running handlers, a\nhandler that subscribed mid-tick would find the loop looking idle and queue a frame of its own, on\ntop of the one the tick was already going to queue when it finished. Two pending frames means\nevery subscriber runs twice per frame, and each doubled tick can double again. A test that\nexpected fewer than forty ticks and saw forty-six is what caught it.\n\n### Only transform and opacity\n\nChanging `width`, `height`, `top`, `left`, or `margin` changes an element's geometry, which\ninvalidates layout — not just for that element but potentially for everything after it in flow —\nand then requires a repaint. Doing that inside an animation means paying layout and paint on every\nsingle frame, for every animating element. A list of fifty items animating `top` is fifty layout\ninvalidations per frame, and layout is the expensive stage.\n\n`transform` and `opacity` are different in kind, not degree. Neither affects the position or size\nof anything in the layout tree, so neither invalidates layout, and both can be applied by the\ncompositor to an already-painted layer. That is why every preset here is restricted to those two\nproperties: not as a stylistic rule but because it is the difference between an animation that\nsurvives a busy main thread and one that does not. `will-change` is used to promote an element\nbefore its animation starts so the first frame does not pay for the promotion — and released the\nmoment the animation ends, because every promoted layer holds its own GPU texture and leaving the\nhint on a few dozen cards is a straightforward way to waste tens of megabytes of video memory. The\nrelease is reference-counted per property, since overlapping animations on one element are normal,\nand it is wired to the `finished` promise rather than to a completion callback, because `finished`\nsettles on cancellation too and a cancelled animation must not leak a layer.\n\n### FLIP: faking layout animation with transforms\n\nThe constraint above raises an obvious objection: what about animations that *are* layout changes\n— a list reordering, a filter removing items, a card being inserted? The browser cannot transition\nan element between two positions it computed from layout, and animating `top`/`left` to fake it is\nexactly what we just ruled out.\n\nFLIP — First, Last, Invert, Play — sidesteps the problem instead of solving it. Measure where\neverything is (**First**), let the layout change happen instantly, measure where everything ended\nup (**Last**), then apply to each element the transform that puts it visually back where it\nstarted (**Invert**) and animate that transform away (**Play**). The elements are at their final\nlayout positions the entire time; only the transform lies about it, and transforms are free of\nlayout. The illusion is exact, and measurable: reorder a list from `[1,2,3]` to `[3,2,1]`, and\nimmediately after the invert every element's bounding rect is identical to what it was before the\nmutation, even though the DOM order has already changed.\n\nThe read/write batching in `flipList` is not stylistic. Every `getBoundingClientRect` issued after\na style write forces the browser to flush pending layout before it can answer. Measuring one\nelement, transforming it, then measuring the next turns a single layout pass into one per element\n— precisely the cost FLIP exists to avoid. So all the reads happen, then the mutation, then all\nthe reads again, then all the writes, in that order and no other.\n\n### Clamped frame deltas\n\nBrowsers throttle `requestAnimationFrame` in background tabs and stop it entirely in some cases.\nWhen the tab comes back, the first frame's timestamp can be seconds after the last one. Passed\nthrough unclamped, a delta of five seconds advances a 600ms tween well past its end, so every\nanimation on the page completes in a single frame and the user returns to a page where everything\nsilently finished — including the entrance animations they never saw. The same thing happens on\nthe main thread without any tab switching, whenever a long synchronous task blocks the loop.\n\nmotio clamps the delta to 64ms, roughly four frames at 60Hz: long enough to absorb ordinary jank\nwithout visibly slowing anything down, short enough to cap a stall. The trade is that after a long\npause an animation is behind wall-clock time — it resumes rather than catches up. That is the\nright trade, because nobody was watching the animation while the tab was hidden, and \"resumes\nsmoothly\" is what a person expects to see. The test for this deliberately blocks the event loop\nfor 150ms and asserts that no subscriber ever observes a delta above the cap.\n\n### seek, and why scroll scrubbing needs no clock\n\nA tween is really two things bolted together: a mapping from progress to values, and a clock that\nadvances progress. `seek(progress)` exposes the first without the second. It sets state and emits\nexactly one frame — it does not subscribe to the ticker, and it deliberately does not settle the\n`finished` promise, so a scrubbed animation can run to its end, back past its start, and forward\nagain without ever being \"done\".\n\nThat separation is what makes scroll scrubbing fall out for free rather than needing a second\nsystem. Scroll is already a stream of progress values; it does not need a clock, and running one\nalongside it would mean two sources of truth fighting over the same element. So `scrollScrub`\nholds no ticker subscription at all; the test suite asserts that `seek` never subscribes, and\nscroll scrubbing is nothing but `seek`. An `IntersectionObserver` gates the scroll listener so\noff-screen\nelements cost nothing, because a page with fifty scrubbed elements otherwise runs fifty\n`getBoundingClientRect` calls on every scroll event, and that is how a scroll handler ends up\nowning the frame budget.\n\nThe same mechanism is what makes the timeline work. It does not *run* its children; it builds them\npaused and drives them with `seek`, while one master tween walks a clock across the sequence. So a\ntimeline of forty staggered elements costs exactly one ticker subscription, and the entire\nsequence can be paused, reversed, scrubbed to 40%, or bound to scroll — because none of the\nchildren own any time of their own.\n\n### Interruption, and why velocity has to be measured\n\nAn animation library that only knows how to start from a declared value cannot handle being\ninterrupted. Grab a card halfway through a `slideIn` and spring it somewhere else, and the spring\nbegins where it was told to begin, at a velocity of zero. The card stops dead for one frame and\nsets off again. It reads as a glitch, because nothing physical changes direction by first coming\nto a halt.\n\nFixing it needs two things the library did not have. The first is a velocity, and a velocity can\nonly be measured, not declared: it is a property of what the element is doing right now, not of\nwhat any animation intended. So every write that goes through `setTransform` or `setOpacity` also\nrecords a timestamped sample, and `velocityOf(el, 'y')` differentiates them. The measurement\nwindow matters more than it looks — differencing two consecutive frames divides a small position\nchange by a jittery interval and produces a number that swings wildly, so the older sample is held\nuntil it is at least 32ms old and the velocity is measured across two or three frames. An\nexponential filter would smooth it too, but it would also lag, and lag is exactly what ruins a\nhandoff: it reports the speed from a moment ago rather than the speed now.\n\nThe second is knowing who is allowed to write. `transform` is a single CSS property, so two\nanimations both writing `y` fight for it every frame and the winner is whichever ticked last. Each\npreset now names the channels it writes, and claiming a channel displaces whoever held it. The\ngranularity is per channel rather than per element on purpose: a magnetic hover writing `x`/`y` and\na fade writing `opacity` are not in conflict and have to keep composing, which is the same reason\n`setTransform` merges through a shared store instead of overwriting.\n\nDisplacement is per element, too, and that falls out of the stagger design. One tween drives a\nwhole staggered list, so cancelling it because a single card was grabbed would freeze the other\n199 mid-flight. Instead the displaced animation stops writing that one element and carries on; when\nit loses its last element it cancels itself, which releases the ticker subscription and the\n`will-change` hints along with it.\n\nWhat the spring then does with the measurement is the part the precomputed-curve design made\nawkward. A spring here is simulated once up front and used as an easing curve, which is what lets\nit be scrubbed, reversed, and placed in a timeline — but a curve is fixed at creation, and elements\ninterrupted at different points of a stagger are moving at different speeds and so want different\ncurves. Each element therefore gets its own simulation, memoized on the velocity rounded to a\nhundredth, which collapses a 200-item list back to a handful of simulations without changing a\nvisible frame. They settle at different times; the tween runs for the longest and each element's\ncurve is stretched across that shared span so it reaches its own resting point on schedule and\nstays pinned there. The single-tween cost model survives intact.\n\nOne honest limitation: the simulation moves a single scalar from 0 to 1, so it can carry exactly\none velocity even when the element is moving on several channels. The channel with the furthest to\ntravel is the one whose velocity is honoured, since it dominates what the motion looks like, and\nthe rest ride the same curve.\n\n### Reduced motion\n\n`prefers-reduced-motion: reduce` is a vestibular accessibility setting, not a taste preference;\nlarge translations and parallax can genuinely make people ill. Every animation respects it by\ndefault, and the handling is deliberate: rather than skipping the animation, the tween emits its\n**final frame** and settles immediately. Layout and final state stay correct, and only the\nmovement is skipped — an element that fades in still ends up visible. The preference is read at\nplay time rather than at creation, so an in-app toggle takes effect on the next animation, and\n`setReducedMotion(true | false | null)` exists because an OS setting is not always something a\nperson can change on a shared or locked-down machine.\n\n## API reference\n\n### `tween(options)`\n\nThe DOM-agnostic primitive. Interpolates a number, or a flat object of numbers, and hands the\nresult to `onUpdate` once per frame.\n\n| Option | Type | Default | Notes |\n| --- | --- | --- | --- |\n| `from` | `number \\| Record<string, number>` | — | Must match `to`'s shape. |\n| `to` | `number \\| Record<string, number>` | — | |\n| `duration` | `number` | `600` | Milliseconds, per iteration. |\n| `delay` | `number` | `0` | |\n| `easing` | `EasingInput` | `'cubicOut'` | Function, name, or `[x1,y1,x2,y2]`. |\n| `repeat` | `number` | `0` | Extra iterations. `Infinity` loops. |\n| `yoyo` | `boolean` | `false` | Reverse each repeat instead of restarting. |\n| `autoplay` | `boolean` | `true` | |\n| `respectReducedMotion` | `boolean` | `true` | |\n| `onUpdate` | `(value, progress, controls) => void` | — | |\n| `onStart` / `onRepeat` / `onComplete` | `(controls) => void` | — | |\n\nReturns chainable controls: `play()`, `pause()`, `resume()`, `reverse()`, `restart()`, `cancel()`,\n`seek(progress)`, plus getters `progress`, `isPlaying`, `duration`, and a `finished` promise.\n\n`finished` **resolves** on cancel rather than rejecting — cancelling is a normal event, not an\nerror, and a rejected promise nobody awaited becomes an unhandled rejection. It settles once.\n\nThe per-frame path allocates nothing: object keys are snapshotted at creation and one output\nobject is reused, so a caller that needs to keep a frame's value must copy it.\n\n### `timeline(options)`\n\n`.add(target, preset, options)` — chainable. Accepts `stagger` and an `at` position: a number\n(absolute ms), `'+=200'` (gap), `'-=200'` (overlap), `'<'` (alongside the previous entry), or\n`'>'` / omitted (after it). A negative `stagger` ripples from the last element backwards.\n\nSame control surface as `tween`, plus `duration`. Entries must be added before playback starts;\n`autoplay` is deferred by a microtask so chained `.add()` calls are measured first. Child\ndurations are read from the children themselves, which is what lets a `spring` sit in a sequence\nand be placed correctly.\n\n### Presets\n\nEvery preset takes `(target, options)` where `target` is a selector, element, NodeList, array, or\nany iterable of those, and returns tween controls. All accept the shared options `duration`,\n`delay`, `easing`, `stagger`, `autoplay`, `respectReducedMotion`, `repeat`, `yoyo`, and the\nlifecycle callbacks.\n\n| Preset | Notable options |\n| --- | --- |\n| `fadeIn` / `fadeOut` | `from`, `to` |\n| `slideIn` | `direction`, `distance`, `fade` |\n| `scaleIn` | `from`, `to`, `fade`, `origin` |\n| `spring` | `from` (transform parts or `'current'`), `to`, `stiffness`, `damping`, `mass`, `velocity` (number or `'inherit'`) |\n| `flipList` | `mutate` (required), `scale` |\n| `drawSVG` | `from`, `to` (fractions), `reverse` |\n| `scrollScrub` | `from`, `to`, `onUpdate`, `startOffset`, `endOffset`, `root` |\n| `textScramble` | `text`, `characters`, `overlap` |\n| `splitText` | `y`, `rotate`, `fade` |\n| `magneticHover` | `strength`, `maxDistance`, `smoothing`, `scale` |\n| `particleBurst` | `count`, `colors`, `spread`, `angle`, `velocity`, `gravity`, `drag`, `size` |\n| `counter` | `from`, `to`, `decimals`, `format`, `locale`, `prefix`, `suffix` |\n\n`magneticHover` is the one preset not built on `tween`, because a tween interpolates toward a value\nfixed when it starts and a magnetic element's target changes with every pointer move. It takes a\nticker subscription directly and releases it when everything is at rest. It returns the same\ncontrol surface; `seek` and `reverse` are documented no-ops.\n\n### Easing\n\n25 easings — `quad`, `cubic`, `quart`, `expo`, `circ`, `back`, `elastic`, `bounce` in `In`, `Out`,\nand `InOut`, plus `linear` — each exported individually and available by name through `easings`.\nAll return exactly `0` at `t=0` and `1` at `t=1`, asserted with `Object.is` so a `-0` fails.\n\n- `cubicBezier(x1, y1, x2, y2)` — matches the CSS signature, so a curve copied from devtools\n  behaves identically. Solved with Newton-Raphson and a bisection fallback for curves whose\n  derivative goes flat.\n- `resolveEasing(value)` — normalizes a function, a name, or four control points.\n\n### Ticker\n\n`subscribe(handler)`, `unsubscribe(handler)`, `activeCount()`, `frameTime()`, `stop()`. Handlers receive\n`(delta, timestamp)`. A handler that throws is removed from the loop and reported once, so one\nbroken animation cannot stop the others or spam an error every frame.\n\n### Reduced motion\n\n`prefersReducedMotion()`, `setReducedMotion(true | false | null)`, `onReducedMotionChange(handler)`\n— the last returns an unsubscribe function and only fires when the *effective* value changes.\n\n### Motion state\n\n`velocityOf(el, channel)` reports how fast a channel is currently moving, in that channel's units\nper second — pixels for `x`/`y`/`z`, degrees for rotations, multiplier per second for `scale`,\nopacity per second for `opacity`. It returns 0 for anything standing still, never animated, or\nstopped long enough that its last samples describe history rather than motion. `forget(el)` drops\nan element's samples and channel claims, which matters for a recycled list row that is about to\nrepresent different data and should not inherit the outgoing row's momentum.\n\n### Utilities\n\n`resolve(target)` normalizes any accepted target to an array of elements. `setTransform(el, parts)`\nmerges translate / rotate / scale / skew through a shared per-element store so two presets on one\nelement compose instead of overwriting each other, and records each written channel for velocity\ntracking. `setOpacity(el, value)` does the same for opacity. `getTransform`, `clearTransform`,\n`getOpacity`, `clearOpacity`, `claimWillChange`, `clearWillChange`.\n\n## Browser support\n\nChrome and Edge 80+, Firefox 74+, Safari 13.1+ — anything with ES modules, optional chaining, and\nnullish coalescing. Nothing is transpiled and no polyfills are shipped.\n\nThree presets need a little more: `scrollScrub` uses `IntersectionObserver`, `magneticHover` uses\nPointer Events, and `particleBurst` uses the canvas 2D context. Everything else needs only\n`requestAnimationFrame` and inline styles.\n\nImporting the package outside a browser is safe. `window`, `document`, `matchMedia`, and\n`requestAnimationFrame` are all guarded, so server rendering and test runners get a module that\nloads, reports `prefersReducedMotion() === false`, resolves selectors to `[]`, and can still drive\na tween by `seek`.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","readmeFilename":"README.md"}