MoveAnimator
The imperative API: animate from TypeScript with the same keyframes, timing vocabulary and reduced-motion handling as the directives. Use it to sequence phases, react to a server response, or drive a View Transition. For a declarative relationship between two template elements, see moveTarget.
animate / set / clear
Order confirmed
import { Component, ElementRef, inject, viewChild } from '@angular/core';
import { MoveAnimator } from 'angular-movement';
@Component({ /* ... */ })
export class Checkout {
readonly #animator = inject(MoveAnimator);
readonly card = viewChild.required<ElementRef<HTMLElement>>('card');
async confirm() {
// ElementRef or Element — no .nativeElement needed.
await this.#animator.animate(this.card(), { scale: [1, 1.04, 1] }, { duration: '240ms' })
?.finished;
// finished always resolves, also when cancelled: no try/catch.
this.#animator.set(this.card(), { opacity: 0.6, y: 8 }); // commit a state instantly
}
reset() {
this.#animator.clear(this.card()); // back to the stylesheet
}
}View Transition reveal
pseudoElement lets the same API animate ::view-transition-new(root), so a theme reveal stays inside your motion system instead of dropping to raw WAAPI. Browsers without View Transitions simply swap instantly; under reduced motion the pseudo-element animation is skipped and the new state appears at once.
Daylight
last run: idle
readonly #animator = inject(MoveAnimator);
readonly #document = inject(DOCUMENT);
reveal(event: MouseEvent, commit: () => void) {
const doc = this.#document;
if (!doc.startViewTransition) return commit(); // progressive enhancement
const transition = doc.startViewTransition(commit);
transition.ready.then(() => {
const { clientX: x, clientY: y } = event;
const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
this.#animator.animate(
doc.documentElement,
{ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },
{
duration: '520ms',
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
pseudoElement: '::view-transition-new(root)',
},
);
});
}
/* global CSS — turn off the default cross-fade while this reveal runs */
::view-transition-old(root),
::view-transition-new(root) { animation: none; mix-blend-mode: normal; }moveTrigger as a handle
<!-- A bare moveTrigger is an imperative handle: no dummy inputs. -->
<div #overlay="moveTrigger" moveTrigger></div>
// Two phases, two timings — per-call options, no input mutation.
await overlay.play({ opacity: [0, 1], scale: [0.6, 1] }, { duration: '420ms' });
await overlay.play({ opacity: [1, 0] }, { duration: '160ms', easing: 'ease-in' });