<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-animation`

- **Import** `import '@aceshooting/lyra-ui/components/lr-animation.js';` (stable tag alias; registers the tag)
- **Class** `LyraAnimation`, also available unregistered from `@aceshooting/lyra-ui/components/media/animation/animation.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 0 parts, 4 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-animation`

Declaratively animates one slotted element through the native Web Animations API.

An initial playing mount creates one native animation and emits one `lr-start`. A real target or
timing change still rebuilds the animation, while replay and reconnect retain their normal
start/finish lifecycle, including reduced motion.

**Properties:**

- `name: string = 'none'` — accepts any animation registry name. The built-in names are `'none' |
'fade-in' | 'fade-out' | 'zoom-in' | 'zoom-out' | 'slide-in-start' | 'slide-in-end' |
'slide-out-start' | 'slide-out-end' | 'slide-in-up' | 'slide-in-down' | 'bounce' | 'pulse' |
'spin' | 'shake'`; `LyraAnimationPreset` remains the exported convenience type for that built-in
  subset. Other strings resolve through the registry key `animation.<name>`. The four built-in
  `-start`/`-end` slide presets are logical: "start" is physically left under `ltr`, right under
  `rtl`.
- `keyframes?: Keyframe[]` (attribute: false) — raw WAAPI keyframes; always wins over `name`.
- `play: boolean = false` (reflected) — playback intent.
- `delay: number = 0`, `duration: number = 1000`, `endDelay: number = 0` (attribute `end-delay`),
  `easing: string = 'linear'`, `fill: FillMode = 'auto'`, `direction: PlaybackDirection = 'normal'`,
  `iterations: number = Infinity`, `iterationStart: number = 0` (attribute `iteration-start`),
  `playbackRate: number = 1` (attribute `playback-rate`) — straight WAAPI timing. `direction` is
  the WAAPI `PlaybackDirection`, unrelated to text direction; `iterations` is always numeric.
- `timingPreset: LyraAnimationTimingPreset = 'custom'` (attribute `timing-preset`, reflected) —
  `'custom' | 'fast' | 'base' | 'ambient'`. Anything other than `'custom'` derives `duration` and
  `easing` from the matching `--lr-transition-*` token (read off computed style and decomposed into
  the numeric ms + easing string WAAPI needs), ignoring the `duration`/`easing` properties.
- `respectReducedMotion: boolean = true` (attribute `respect-reduced-motion`, reflected) — under
  `prefers-reduced-motion: reduce`, caps playback at one iteration and calls `finish()` immediately
  instead of playing, so the target snaps to its resolved end state; `lr-start`/`lr-finish` still
  fire in order.
- `playOnVisible: boolean = false` (attribute `play-on-visible`, reflected) — starts playback via
  `IntersectionObserver` once the target intersects. `playOnVisibleRepeat: boolean = false`
  (attribute `play-on-visible-repeat`, reflected) — re-plays on each re-entry and pauses on exit;
  when unset the observer disconnects after the first trigger. `root: Element | null = null` and
  `threshold: number | readonly number[] = 0` (both attribute: false) plus
  `rootMargin: string = '0px'` (attribute `root-margin`) configure that observer. Threshold arrays
  are frozen snapshots, retain only finite values from 0 through 1, and inspect at most 1,000
  candidates per assignment; invalid scalar thresholds normalize to `0`.
- `currentTime: CSSNumberish` — the underlying `Animation.currentTime` (`0` when no animation
  exists); writable and forwarded when one exists. Non-finite numeric assignments are ignored.

**Methods:** `start()` (sugar for `play = true` — named `start` because `play` is already a
property), `pause()` (`play = false`), `finish()`, `cancel()`. `cancel()` leaves the target reverted
to its own CSS: the `play = false` that the resulting `lr-cancel` sets never re-pauses the now-idle
`Animation`, which per the Web Animations API would un-cancel it back to keyframe zero and freeze the
target there.

**Events:** `lr-start` (a new animation was created and playback began/restarted), `lr-finish`
(natural end, including the reduced-motion instant-finish path), `lr-cancel` (the public `cancel()`
method or external cancellation). `lr-finish`/`lr-cancel` both reset `play` to `false`.

**Slots:** default slot — the element to animate. A second slotted element is accepted without
error but ignored.

**CSS parts:** none (`:host { display: contents }`; the animated element is light-DOM content).

**Themeable custom properties:** `--lr-animation-slide-distance` (default `100%` — travel distance
for the slide presets), `--lr-animation-zoom-scale` (default `0.5` — start/end scale for
`zoom-in`/`zoom-out`), `--lr-animation-bounce-height` (default `25%` — peak lift of `bounce`),
`--lr-animation-shake-distance` (default `4%` — horizontal travel of `shake`).

**Animation registry:** every named preset resolves the public registry key
`animation.<name>` before using its built-in keyframes. A per-element `setAnimation()` registration
wins over `setDefaultAnimation()`; a keyframes-only registration retains this component's
property- or token-derived timing, and `rtlKeyframes` is selected from the live inherited text
direction. Both setters return an idempotent cleanup. Passing `null` disables interpolation but
still emits `lr-start` then `lr-finish`, so code sequencing work from the lifecycle does not stall.
Direct `keyframes` property input remains the strongest, instance-local override and does not use a
registry name. Register an override before the first render when creating an animation
programmatically. For an already-rendered `<lr-animation>`, the override is selected on its next
normal rebuild (a keyframe/timing/direction change or reconnect); the registry never mutates a
native timeline that is already running.

```js
import "@aceshooting/lyra-ui/components/media/animation/animation.js";
import { setAnimation } from "@aceshooting/lyra-ui/utilities/animation-registry.js";

const animation = document.createElement("lr-animation");
animation.name = "slide-in-start";
animation.iterations = 1;
animation.innerHTML = "<span>Registry-controlled content</span>";
const release = setAnimation(animation, "animation.slide-in-start", {
  keyframes: [
    { transform: "translateX(calc(-1 * var(--lr-size-2rem)))" },
    { transform: "translateX(0)" },
  ],
  rtlKeyframes: [
    { transform: "translateX(var(--lr-size-2rem))" },
    { transform: "translateX(0)" },
  ],
});
document.body.append(animation);
animation.start();
// release() restores the previous registration.
```

**Optional peer deps:** none.

**Known gotchas:**

- `iterations` defaults to `Infinity` (mirrors the upstream Web Awesome/Shoelace contract) — a named
  preset plays forever unless you set `iterations="1"`.
- changing any timing/keyframe property rebuilds the animation from scratch; the rebuild's internal
  `cancel()` is deliberately silent (no `lr-cancel`), only the public `cancel()` emits.
- the slide presets re-resolve when an inherited text-direction change is observed. The old native
  animation is replaced rather than mirrored in place, so its timeline restarts from the new edge.
- reduced-motion handling is entirely in JS; the shared shadow-DOM reduced-motion CSS block can't
  reach light-DOM slotted content driven by `Element.animate()`.
