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

# `lr-animated-image`

- **Import** `import '@aceshooting/lyra-ui/components/lr-animated-image.js';` (stable tag alias; registers the tag)
- **Class** `LyraAnimatedImage`, also available unregistered from `@aceshooting/lyra-ui/components/media/animated-image/animated-image.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** 5 parts, 5 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-animated-image`

An animated GIF/APNG/WebP with a play/pause control, frozen to a captured still frame at rest and
automatically under `prefers-reduced-motion: reduce`.

**Properties:**

- `src: string = ''` — re-validated through `safeMediaSrc()` (same allowlist as `lr-media-card`)
  before reaching the real `<img src>`.
- `alt?: string` — forwarded to the live image and frozen canvas. An absent or explicitly empty
  value keeps both visual owners decorative; a nonempty value names whichever one is exposed. The
  independent play/pause action still uses localized context when no nonempty `alt` is available.
- `play: boolean = false` — the caller's _intent_ (reflected).
- `playing: boolean` (readonly getter, reflected as a `playing` host attribute) — the _effective_
  state after reduced-motion arbitration: `play && !(respectReducedMotion && <OS prefers reduce>)`.
  It is a genuine getter-only property, so assigning to it from a strict JavaScript module throws a
  `TypeError`; drive playback via `play`.
- `respectReducedMotion: boolean = true` (reflected, attribute `respect-reduced-motion`) — while
  `true` and the OS reports `prefers-reduced-motion: reduce`, playback stays frozen and
  `[part="play-button"]` is `disabled` regardless of `play`.
- `accessibleLabel: string = ''` (attribute `aria-label`) — when the host attribute is present,
  including explicitly empty, it overrides `[part="play-button"]`'s computed Play/Pause label
  verbatim in _both_ states (it does not itself vary by state). Never
  touches the image's `alt`/the canvas's `aria-label`. For state-sensitive custom wording, override
  the `playWithContext`/`pauseWithContext`/`animatedImageDefaultAlt` strings instead.

**Methods:** `focus(options?)` and `blur()` forward to the play/pause button.

**Events:** `lr-load` (the live `<img>` loaded; fires again on every successful `src` change),
`lr-error` (native decode failure, or a non-empty `src` that failed the safe-URL check — never for
an empty `src`), `lr-play`/`lr-pause` (real transitions of the effective `playing` value only, so a
`play = true` that reduced motion blocks emits nothing, while a live reduced-motion change that
forces a freeze does emit `lr-pause`). Internal `focus`/`blur` are relayed exactly once as
owner-realm native `FocusEvent`s (bubbling and composed, preserving `relatedTarget`).

**Slots:** `play-icon`, `pause-icon` — decorative custom glyphs for the frozen/paused and playing
states. Both stay mounted and are toggled via the native `hidden` attribute. Their assigned content
renders in an inert, `aria-hidden`, pointer-transparent sibling layer over the named play/pause
button, so do not use either slot for a second interactive control.

**CSS parts:** `base` (positioning context), `image` (the live `<img>`), `canvas` (the
frozen-frame `<canvas>`, shown in place of `image` while not playing), `control-box` (the
backgrounded circle around the button; only rendered once loaded and error-free), `play-button`.

**Themeable custom properties:** upstream `--control-box-size` and `--icon-size` feed
`--lr-animated-image-control-box-size` (default
`var(--lr-icon-button-size)`), `--lr-animated-image-icon-size` (default
`calc(var(--lr-icon-button-size) * 0.35)`), `--lr-animated-image-max-height` (default
`var(--lr-size-20rem)` — caps the rendered media's block-size, same contract as
`--lr-media-card-max-height`).

**Optional peer deps:** none.

**Known gotchas:**

- the freeze frame is captured once per successful `src` load, in the `<img>`'s own `load` handler
  (a DPR-aware `drawImage()`), not re-captured on each pause — pausing always reverts to that first
  frame, never to the frame that was on screen.
- both `image` and `canvas` stay mounted at all times (never `display: none`/removed) so the
  browser's native decode loop keeps running while visually covered; only opacity and `aria-hidden`
  swap.
- reduced-motion arbitration is re-evaluated on every reconnect, so a preference change made while
  the element was detached cannot leave `playing` stale.

**Additional API surface:**

- `click()` — Activate the play/pause control.
