Ani
Guides

Migrating from 1.x

Move a codebase from ANI 1.x to 2.0 in one cut. Run the codemod, fix what it reports, and check the behavior changes that type-checking cannot catch.

Why 2.0

ANI 2.0 computes every frame first and writes the DOM once, in a commit phase, so layout reads and writes never interleave. A rafGroup drives a whole list with one plan and one clock subscription. rafTimeline and waapiTimeline share one semantics: the same options, run results and validation. Reduced motion is honored by default, and every duration, delay and offset is in seconds, including the delay of play(). The price is a breaking API: 1.x names are removed rather than deprecated, and 1.x call shapes throw instead of being guessed. This guide moves a 1.x codebase over in one pass.


Run the codemod first

The codemod rewrites every call site that has a safe 2.0 form and reports the rest with file, line and column.

npx @freestylejs/ani-codemod src

Pass every directory that contains ANI code. Directories are walked recursively (node_modules, dist, .git and .next are skipped). The codemod handles .ts, .mts, .cts, .tsx, .js, .mjs, .cjs, .jsx, .vue, .svelte, .md and .mdx files.

Commit before you run it, and run it once. The codemod cannot tell seconds that were already converted from milliseconds, so a second run divides every delay again.

Preview with --dry

--dry prints the warnings and the summary without writing any file.

npx @freestylejs/ani-codemod --dry src

Each warning has the form file:line:column [rule] message, followed by N files changed, M warnings. The exit code is 0, or 1 on a usage, IO or syntax error (other files are still processed).

Read every warning

Nothing is changed silently: whatever the codemod cannot decide is left as it was and reported. Each rule has a section below.

RuleSection
ambiguous-playAmbiguous play
reset-argumentsReset arguments
property-resolverProperty resolver
context-removedContext removed
clock-resetClock reset
clock-createClock create
clock-configure-argumentClock configure argument
react-use-ani-refReact useAniRef

Type-check next

Removed APIs do not type-check in 2.0. After the codemod, run tsc --noEmit and fix what is left with the table below. Untyped code gets a different safety net: 1.x call shapes throw a TypeError at the first call.


Every change at a glance

The Codemod column says what ani-codemod does:

  • yes: the call site is rewritten.
  • partly: only some call shapes are rewritten, or the code is changed and a warning asks for a manual follow-up.
  • no: the code is not touched. A warning may still point at it.
1.x2.0Codemod
import { a } from '@freestylejs/ani-core'import * as a from '@freestylejs/ani-core'yes
T (T.linear(), T.spring(...))a.timing, or import { timing }no
a.dynamicTimeline(node)a.rafTimeline(node)yes
a.timeline(node), a.webTimeline(node)a.waapiTimeline(node)yes
a.rafTimeline(node, clock)a.rafTimeline(node, { clock })yes
a.sequence(children, timing, id), a.parallel(children, timing, id)a.sequence(children, { timing, id })yes
a.loop(node, count, timing, id)a.loop(node, count, { timing, id })yes
a.stagger(children, offset, timing, id)a.stagger(children, { offset, timing, id })yes
a.delay(seconds, id)a.delay(seconds, { id })yes
a.ani(props, id)a.ani({ ...props, id })yes
loop(node, n) with keyframes / durations entries per iterationone entry per ani / delay node as written in the treeno
timeline.getCurrentValue(), timeline.getCurrentVelocity(), timeline.currentConfigtimeline.value, timeline.velocity, timeline.configyes
timeline.play(config, canBeIntercepted)timeline.play({ ...config, interrupt })yes
play(x, y) that matches no known 1.x shapedecide RAF or WAAPI by hand (Ambiguous play)partly
timeline.reset(notify, unsubscribeClock)timeline.reset()partly
delay: 500 in a play or transitionTo config (milliseconds)delay: 0.5 (seconds)partly
RAF play({ propertyResolver })timeline.bind(target, { resolver })no
RAF play({ context })removed (Context removed)no
play() and transitionTo() return nothingthey return a Promise<RunResult> that never rejectsno
onUpdate callbacks run inside the frame updatethey run in the commit phaseno
WAAPI play(element, { propertyResolver, keyframeEffect })play(element, { resolver, effect })yes
WAAPI timeline.nativeAnimationtimeline.animations[0]yes
WAAPI play() returns Animation | null, one target per timelineit returns a Promise<RunResult> and takes one element or a listno
createStates({ initialFrom })createStates({ initialValue })yes
machine.onTimelineChange((timeline) => ...)machine.onTransition((state, timeline) => ...)yes
machine.transitionTo(state, config, canBeIntercepted)machine.transitionTo(state, { ...config, interrupt })yes
AnimationClock.configure(0.1)defaultClock.configure({ maxDeltaTime: 0.1 })yes
AnimationClock.create()defaultClockyes
AnimationClock.create(maxDeltaTime)defaultClock.configure({ maxDeltaTime }), or createClock({ maxDeltaTime })no
AnimationClock.reset()resetDefaultClock() from @freestylejs/ani-core/testing, or dispose what you createdno
clock.setTiming(options)clock.configure(options)no
Error messages prefixed [AnimationClock]prefixed [Clock]no
RafAniTimeline<G, Ctx>, WebAniTimeline<G> (types)RafTimeline<G>, WaapiTimeline<G>no
RafTimelineConfig<G, Ctx>, WebAniTimelineConfig<G>RafTimelineConfig<G>, WaapiPlayConfig<G>no
value instanceof RafAniTimeline, value instanceof WebAniTimelineisRafTimeline(value), isWaapiTimeline(value)no
Node classes as values (SegmentNode, SequenceNode, ...)removed; the types remain, build nodes with a.ani, a.sequence, ...no
dynamicSpring instance methods init() and reset()removed; the spring state lives in the timelineno
EventManager, calculateSegmentState, PlanRuntime, TimelineBaseremovedno
applyStyleSheet(element, style)timeline.bind(element) (createStyleSheet stays for server rendering)no
Binding events and cleanupEvents optionsyour framework's event handlersno
Event handler context ctx.play(...), ctx.currenttimeline.play(...), timeline.valueno
React useAniRef(ref, { timeline, ... }) returning a controlleruseAniRef(timeline, options) returning { ref, style }no
React useAniStates(ref, props) returning [{ state, timeline }, transitionTo]useAniStates(config) returning { ref, style, state, transitionTo, machine }no
Vue, Solid and Svelte useAniRef, createAniRef returning [ref, controller]useAniRef(timeline, options) returning { ref, style } (Svelte: { action, style })no
Vue, Solid and Svelte useAniStates, createAniStates returning [ref, { state, timeline }, transitionTo]one config in, { ref, style, state, transitionTo, machine } out (Svelte: action)no
initialFrom in binding configsinitialValueno

useAni and createAni keep their [value, controller] tuple. The timeline you pass them is still never disposed by the binding.


Manual changes

Ambiguous play

ambiguous-play marks a play or transitionTo call whose arguments match no known 1.x shape, such as timeline.play(x, y). The codemod tells the engines apart by the arguments: the WAAPI form is play(element, { ... }), the RAF form is play(config) or play(config, true | false).

Decide which engine the receiver is. For RAF, the flag becomes the interrupt key of the config. For WAAPI, the element comes first.

import * as a from '@freestylejs/ani-core'

const node = a.ani({ to: { opacity: 1 }, duration: 0.3 })
const raf = a.rafTimeline(node)
const waapi = a.waapiTimeline(node)
const element = document.createElement('div')
const config = { from: { opacity: 0 } }
const interruptible = false

raf.play({ ...config, interrupt: interruptible })
waapi.play(element, config)

Reset arguments

reset-arguments means reset was called with arguments and the codemod removed them. In 1.x, reset(false) skipped the notification and reset(true, false) kept the timeline on the clock. In 2.0, reset() takes no arguments and always notifies subscribers once, with status IDLE and an empty state.

Check that your onUpdate callbacks handle that notification. A callback that wrote the state to the DOM should skip the IDLE frame.

import * as a from '@freestylejs/ani-core'

const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))

const element = document.createElement('div')

const stop = timeline.onUpdate(({ state, status }) => {
    if (status === 'IDLE') return // reset() notifies with an empty state
    element.style.opacity = String(state.opacity)
})

timeline.reset()
stop()

To reset without notifying, unsubscribe first or dispose the timeline.

Property resolver

property-resolver marks a RAF play or transitionTo config with propertyResolver. The key is removed from play configs because play() no longer writes styles. The style mapping belongs to the binding: bind(target, { resolver }).

import * as a from '@freestylejs/ani-core'

const timeline = a.rafTimeline(a.ani({ to: { progress: 100 }, duration: 1 }))
const element = document.createElement('div')

timeline.bind(element, {
    resolver: {
        progress: (value) => ({ key: '--progress', value: `${value}%` }),
    },
})
timeline.play({ from: { progress: 0 } })

The framework bindings take the same resolver in their options: useAniRef(timeline, { resolver }). See Styles & Building Bindings.

The WAAPI option is not removed: propertyResolver is renamed resolver (the codemod does this).

Context removed

context-removed marks a RAF config with context. The value was never read by the engine and the key is gone. Remove it and pass the values your callbacks need through a closure, the timeline or the nodes.

Clock reset

clock-reset marks AnimationClock.reset(), which has no direct equivalent in application code. Dispose the timelines and clocks you created instead; the clock stops its loop when the last subscriber leaves.

In tests, use resetDefaultClock() from @freestylejs/ani-core/testing. It disposes the shared clock and restores its defaults while keeping the same instance. See Testing.

import { resetDefaultClock } from '@freestylejs/ani-core/testing'
import { afterEach } from 'vitest'

afterEach(() => {
    resetDefaultClock()
})

Clock create

clock-create marks AnimationClock.create(maxDeltaTime) with an argument. AnimationClock.create() without arguments is rewritten to defaultClock. With an argument, choose between changing the shared clock and making your own.

import { createClock, defaultClock } from '@freestylejs/ani-core'

// the shared clock, reconfigured
defaultClock.configure({ maxDeltaTime: 0.1 })

// an independent clock for a separate engine
export const ownClock = createClock({ maxDeltaTime: 0.1 })

configure takes an options object. A bare number is a type error, and in untyped code it throws ANI_INVALID_OPTIONS.

Clock configure argument

clock-configure-argument marks AnimationClock.configure(...) called with an argument that is neither a numeric literal nor an object literal. In ANI 2, configure takes { lagThreshold, maxDeltaTime }. If the value is a number, pass { maxDeltaTime: value }.

React useAniRef

react-use-ani-ref marks useAniRef(ref, props). The hook no longer takes a ref object. It takes the timeline and returns a ref callback and an initial style. See React.

Delay held in variables

The codemod converts delay only in object literals written inline in the play or transitionTo call. A config held in a variable, built by a helper or read from settings keeps its milliseconds, and TypeScript cannot tell: both are number. A delay: 500 that is not converted waits 500 seconds.

Search for delay in every config you build outside the call and divide by hand.

// 1.x
const config = { from: { opacity: 0 }, delay: 500 }

// 2.0
const config = { from: { opacity: 0 }, delay: 0.5 }

The same applies to the delay option of WAAPI play() and of transitionTo(). The delay node, duration and stagger offsets were already in seconds.

Classes and instanceof

The timeline classes are no longer exported. Tell the engines apart with the type guards.

import { isRafTimeline, isWaapiTimeline } from '@freestylejs/ani-core'

export function engineOf(value: unknown): 'raf' | 'waapi' | null {
    if (isRafTimeline(value)) return 'raf'
    if (isWaapiTimeline(value)) return 'waapi'
    return null
}

Replace the types too: RafAniTimeline becomes RafTimeline, WebAniTimeline becomes WaapiTimeline. isRafTimeline is also true for the controller facade that the framework bindings hand out.

Events

EventManager and the events option of the bindings are removed. They attached listeners for you and passed a context (ctx) that held the controller methods and ctx.current. In 2.0 the DOM is the framework's job: attach handlers the way the framework does, and call the timeline directly. ctx.current becomes timeline.value.

Without a framework, use addEventListener and remove the listener when the owner goes away.

import * as a from '@freestylejs/ani-core'

const timeline = a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
const element = document.createElement('div')

const unbind = timeline.bind(element)
const onPointerDown = (event: PointerEvent) => {
    void timeline.play({ from: { translateX: event.clientX } })
}
element.addEventListener('pointerdown', onPointerDown)

// when the owner goes away
element.removeEventListener('pointerdown', onPointerDown)
unbind()
timeline.dispose()

Writing styles

applyStyleSheet(element, style) is removed. timeline.bind(element) writes the value in the commit phase, only for declarations that changed. createStyleSheet(values, resolver?) stays for the first server-rendered paint.


Framework bindings

All four bindings change in the same way:

  • useAniRef (createAniRef in Solid) takes the timeline and an options object, and returns { ref, style }. Svelte returns { action, style }.
  • style is the initial inline style of initialValue. Put it on the element so the server renders it.
  • events and cleanupEvents are gone. Use the framework's event handlers.
  • useAniStates (createAniStates) takes one config and returns an object.
  • useTimeline (createTimeline) is new. It creates a timeline, group or follower and disposes it with the component.

React

React no longer takes a ref object: useAniRef returns the ref callback, and the component owns the timeline with useTimeline.

// 1.x
const ref = useRef<HTMLDivElement>(null)
const timeline = useMemo(() => a.dynamicTimeline(node), [])
const controller = useAniRef(ref, {
    timeline,
    initialValue: { translateX: 0 },
    events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
return <div ref={ref} />
import * as a from '@freestylejs/ani-core'
import { useAniRef, useTimeline } from '@freestylejs/ani-react'

const node = a.ani({ to: { translateX: 300 }, duration: 0.5 })

export function Box() {
    const timeline = useTimeline(() => a.rafTimeline(node))
    const { ref, style } = useAniRef(timeline, {
        initialValue: { translateX: 0 },
    })

    return (
        <div
            ref={ref}
            style={style}
            onPointerDown={(event) => {
                void timeline.play({ from: { translateX: event.clientX } })
            }}
        />
    )
}

useAniStates loses the ref argument and returns an object:

// 1.x
const ref = useRef<HTMLButtonElement>(null)
const [{ state }, transitionTo] = useAniStates(ref, { initial: 'idle', initialFrom: { opacity: 0.7 }, states })
return <button ref={ref} data-state={state} />
import * as a from '@freestylejs/ani-core'
import { useAniStates } from '@freestylejs/ani-react'

const states = {
    idle: a.ani({ to: { opacity: 0.7 }, duration: 0.5 }),
    hover: a.ani({ to: { opacity: 1 }, duration: 0.3 }),
}

export function StateButton() {
    const { ref, style, state, transitionTo } = useAniStates({
        initial: 'idle',
        initialValue: { opacity: 0.7 },
        states,
    })

    return (
        <button
            ref={ref}
            style={style}
            data-state={state}
            onMouseEnter={() => void transitionTo('hover')}
            onMouseLeave={() => void transitionTo('idle')}
        />
    )
}

The timeline member is gone from the result. Read the active timeline with machine.timeline().

Vue

useAniRef returns { ref, style } instead of [elementRef, controller]. Bind both in the template: :ref="ref" :style="style".

// 1.x
const [elementRef, controller] = useAniRef({
    timeline: myTimeline,
    initialValue: { translateX: 0 },
    events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
// <div :ref="elementRef" />
<script setup>
import * as a from '@freestylejs/ani-core'
import { useAniRef, useTimeline } from '@freestylejs/ani-vue'

const timeline = useTimeline(() =>
    a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
)
const { ref, style } = useAniRef(timeline, {
    initialValue: { translateX: 0 },
})
</script>

<template>
    <div
        :ref="ref"
        :style="style"
        @pointerdown="timeline.play({ from: { translateX: $event.clientX } })"
    />
</template>

useAniStates returns { ref, style, state, transitionTo, machine } instead of [elementRef, { state, timeline }, transitionTo], and state is a readonly ref.

Solid

createAniRef takes the timeline (or an accessor of one) and returns { ref, style }. style is an accessor, so call it: style={style()}.

// 1.x
const myTimeline = createMemo(() => a.dynamicTimeline(node))
const [ref, controller] = createAniRef({
    timeline: myTimeline,
    initialValue: { translateX: 0 },
    events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
return <div ref={ref} />
import * as a from '@freestylejs/ani-core'
import { createAniRef, createTimeline } from '@freestylejs/ani-solid'

const node = a.ani({ to: { translateX: 300 }, duration: 0.5 })

export function Box() {
    const timeline = createTimeline(() => a.rafTimeline(node))
    const { ref, style } = createAniRef(timeline, {
        initialValue: { translateX: 0 },
    })

    return (
        <div
            ref={ref}
            style={style()}
            onPointerDown={(event) => {
                void timeline.play({ from: { translateX: event.clientX } })
            }}
        />
    )
}

createAniStates returns { ref, style, state, transitionTo, machine }, and state is an accessor: state().

Svelte

useAniRef returns { action, style } instead of [action, controller]. Use use:action and pass style to the element.

// 1.x
const [refAction, controller] = useAniRef({
    timeline: myTimeline,
    initialValue: { translateX: 0 },
    events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
// <div use:refAction />
<script>
  import * as a from '@freestylejs/ani-core'
  import { useAniRef, useTimeline } from '@freestylejs/ani-svelte'

  const timeline = useTimeline(() =>
    a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
  )
  const { action, style } = useAniRef(timeline, {
    initialValue: { translateX: 0 },
  })
</script>

<div
  use:action
  {style}
  on:pointerdown={(event) => timeline.play({ from: { translateX: event.clientX } })}
/>

useAniStates returns { action, style, state, transitionTo, machine } instead of [action, { state, timeline }, transitionTo], and state is a store: $state.


Behavior changes

These changes compile and run, so the type checker and the codemod cannot find them. Check each one.

Delay unit

The delay of play() and transitionTo() is in seconds, on RAF and WAAPI. See Delay held in variables.

Reduced motion

Animations honor prefers-reduced-motion by default. With the system preference on, rafTimeline, waapiTimeline, rafGroup, follow and the timelines made by createStates apply the policy 'jump': play() or set() validates as usual, then the run completes at once in its end state (follow on the next frame). Bound targets are written, listeners are notified once, and the promise resolves 'ended'. The exception is an endless run — a tree containing loop(..., Infinity), whose duration is Infinity; repeat: Infinity on a finite tree is not endless — which holds its first end state with status PAUSED and keeps its promise pending instead of resolving 'ended'.

Opt out per timeline with the reducedMotion option. Keep 'ignore' for motion that carries information (a progress indicator), or shorten instead of skipping with { maxDuration }.

import * as a from '@freestylejs/ani-core'

const node = a.ani({ to: { width: 100 }, duration: 1 })

// essential motion: unchanged behavior
const progress = a.rafTimeline(node, { reducedMotion: 'ignore' })

// a short transition instead of an instant change
const panel = a.waapiTimeline(node, { reducedMotion: { maxDuration: 0.2 } })

The preference lives on the clock. To change it for every timeline on a clock, create the clock with createClock({ reducedMotion: false }) and pass it as { clock }. The policy is separate from the clock: pass reducedMotion to rafTimeline, waapiTimeline, rafGroup, follow or createStates, which forwards it to every state's timeline.

rafGroup and follow honor the same preference: rafGroup accepts 'jump', 'ignore' and { maxDuration }; follow accepts 'jump' and 'ignore' (a spring has no duration). Only drive() does not read the preference: a driven run is progress-driven, so the user controls the motion. Where matchMedia does not exist (servers, plain test environments) the preference is off, so existing tests keep their timings. See Reduced motion.

Commit phase

update only computes. Bound styles are written and onUpdate callbacks run in the commit phase, after every timeline on the clock has computed. A frame notifies once: the frame in which a run ends notifies once with ENDED and the final value, and a frame that wraps a repeat notifies once with the post-wrap value.

Code that depended on the 1.x order needs a look:

  • A callback that reads another timeline's value sees that timeline's value of the same frame, because every timeline has computed before any callback runs.
  • Code that mixed layout reads with style writes should move the reads to clock.read(fn). See clock.
  • play(), seek() and reset() still notify synchronously.
import * as a from '@freestylejs/ani-core'

const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))
const element = document.createElement('div')

// styles are written before the listeners of the same notification run
timeline.bind(element)
timeline.onUpdate(({ status }) => {
    if (status === 'ENDED') element.dataset.done = 'true'
})

Promises that never reject

play() and transitionTo() return a Promise<RunResult> that resolves once per run with 'ended', 'interrupted', 'skipped', 'reset' or 'disposed'. The promise never rejects, so .catch() handlers and try blocks around await never run. An invalid config still throws synchronously, before any promise exists.

import * as a from '@freestylejs/ani-core'

const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))

export async function reveal(): Promise<boolean> {
    const result = await timeline.play({ from: { opacity: 0 } })
    return result === 'ended' // not 'interrupted', 'reset' or 'disposed'
}

Treat 'interrupted' as normal control flow: a second play() over a running timeline settles the first run with it.

1.x call shapes throw

Un-migrated JavaScript fails loudly. Each of these throws a TypeError that names the 2.0 form, before anything changes:

a.sequence(children, timing)          // use { timing }
a.parallel(children, timing, id)      // use { timing, id }
a.loop(node, count, timing)           // use { timing }
a.stagger(children, 0.1)              // use { offset: 0.1 }
a.ani(props, id)                      // put id into props
a.delay(1, id)                        // use { id }
a.rafTimeline(node, clock)            // use { clock }
timeline.play(config, false)          // use { ...config, interrupt: false }
timeline.play({ from, context })      // context is removed
timeline.play({ from, propertyResolver })  // use bind(target, { resolver })
timeline.reset(true)                  // reset() takes no arguments
machine.transitionTo(state, config, false) // use { ...config, interrupt: false }
a.createStates({ initialFrom })       // use initialValue

A positional argument is never guessed. Four changes cannot be detected at runtime: the delay unit, reduced motion, the commit phase and the loop overrides.

Reset always notifies

reset() notifies subscribers once with status IDLE and an empty state, and the non-notifying reset(false) is gone. See Reset arguments.

Error message prefixes

Messages are prefixed with the name of the API that threw. The clock prefix changed from [AnimationClock] to [Clock], and the new TypeErrors name the 2.0 form. Code or tests that match message text break. Match errors by class (TypeError, RangeError) or by AniError.code instead.

Loops and overrides

keyframes and durations hold one entry per ani or delay node as written in the tree. The child of a loop is one entry, whatever the loop count, and the entry applies to every iteration. In 1.x a loop was written out as copies and every iteration had its own entry. Remove the extra entries from arrays you pass to looped trees. loop(child, Infinity) is now allowed. See loop.

WAAPI runs

A waapiTimeline can play one element or a list, with a per-target stagger. Playing over a running timeline commits the current styles of every old animation and cancels it, so the element continues from the value it showed. from is a record of CSS properties. See waapiTimeline.

Engine corrections

Several 1.x behaviors were defects and are fixed in the same release. They are not API changes, but they can change what you see. Each row links to the page that describes the 2.0 behavior.

Area2.0 behavior
Overlapping segmentsThe segment that started most recently produces the value. See rafTimeline.
Invalid configsplay() throws synchronously and leaves the timeline untouched. See Validation.
Valuesvalue is a frozen snapshot; do not mutate it. All onUpdate callbacks of one notification receive the same read-only state.
Interruptionplay() over a PLAYING timeline inherits its velocity. Pass velocity: { x: 0 } to start from rest.
SpringsdynamicSpring integrates in fixed substeps and ends when settled, or at its duration. See timing.
Frame timeLag smoothing follows the wall clock under jank and resumes after a suspension. maxDeltaTime is unlimited by default. See clock.
State machinesReplaced timelines are disposed; an unknown state throws a RangeError. See createStates.
Nodesani, delay and stagger throw a RangeError for negative or non-finite durations and offsets. loop needs an integer count >= 0 or Infinity.
Server renderingplay() without requestAnimationFrame does not throw. See SSR.

Checklist

Work through the list before you ship.

  • Commit, then run the codemod once on every directory with ANI code.
  • Read every warning and fix it with the matching section above.
  • Search for delay in configs held in variables and divide by 1000.
  • Run tsc --noEmit and replace every removed name from the table.
  • Replace instanceof checks with isRafTimeline and isWaapiTimeline.
  • Replace events and EventManager with framework handlers, and call the timeline from them.
  • Update every binding call: useAniRef, useAniStates and their Vue, Solid and Svelte equivalents.
  • Put style from the bindings on the elements, and create owned timelines with useTimeline.
  • Check code that awaits play(): handle 'interrupted', drop the .catch().
  • Decide the reduced motion policy of every essential animation.
  • Review onUpdate callbacks for commit-phase ordering and for the IDLE notification of reset().
  • Remove per-iteration entries from keyframes and durations of looped trees.
  • Match errors by class or AniError.code, not by message text.
  • Run your tests with createManualClock and resetDefaultClock. See Testing.