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

# `lr-video-playlist`

- **Import** `import '@aceshooting/lyra-ui/components/lr-video-playlist.js';` (stable tag alias; registers the tag)
- **Class** `LyraVideoPlaylist`, also available unregistered from `@aceshooting/lyra-ui/components/media/video-playlist/video-playlist.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `experimental` since `8.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)
- **Deprecated part** `base` since `8.0.0`; use part `::part(video-playlist)`; removal not before `10.0.0` — The video-playlist part identifies the root component explicitly; base remains on the same root node for migration compatibility. That version is a policy floor, not a plan: `wa-video-playlist` still publishes its own deprecated `base` part, so this alias is removed only when upstream's is.
- **Optional peers** `dompurify` — see `llms/peers.md`
- **Themeable via** 7 parts, 2 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-video-playlist`

Experimental direct-child `<lr-video>` playlist with a visible current-video stage and
keyboard-navigable item list. It mirrors the public Web Awesome Video Playlist API under the `lr-`
prefix. Import the granular registration entry with
`import '@aceshooting/lyra-ui/components/media/video-playlist/video-playlist.js'`.

**Properties:** `controls: 'none' | 'standard' | 'full' = 'full'` (reflected and forwarded to every
direct child), and `iconLibrary: string = 'system'` (attribute `icon-library`, non-reflected and
forwarded). Lyra additionally provides `autoAdvance: boolean = true` (attribute `auto-advance`;
`auto-advance="false"` disables completion-driven navigation) and `repeat: 'none' | 'one' | 'all' =
'none'`. Keeping `autoAdvance` true preserves the mirrored behavior in which an ended video starts
the next one. `repeat="one"` restarts the current video; `repeat="all"` wraps the final video to the
first. `items: readonly LyraVideoPlaylistItem[] = []` (attribute: false) is deterministic
first-render row metadata with `{ title, poster?, duration?, unavailable? }`, indexed to the direct
video children. Assign the same value before the server and browser first render. Seeded rows stay
visible but disabled while live children are unavailable; after hydration, each child's live
title/poster/duration and native `inert` state become authoritative in a corrective update that
reuses the server-rendered row nodes. Once live children have been observed, later removal does not
make stale seed rows reappear.

**Methods:** `goTo(index)` selects a finite integer direct-child index; invalid, fractional, and
inert-child indexes are no-ops. Calling it for the current index still emits `lr-video-change`, matching
the mirrored contract. `next()` and `previous()` select the next or previous enabled child when one
exists. `focus(options?)`, `blur()`, and `click()` forward to the playlist row that currently owns
the optional-arrow navigation cursor (falling back to the first enabled row). This forwarding is a
convenience; every enabled row is independently reachable through ordinary sequential Tab order.

**Events:** internal `focus`/`blur` from a playlist row are relayed exactly once as owner-realm
native `FocusEvent`s (bubbling and composed, preserving `relatedTarget`).
`lr-video-change` is bubbling and composed but non-cancelable, with exact
detail `{ previousIndex, currentIndex, video }`. `video` is a fresh detached, recursively frozen plain-data snapshot with
exact shape `{ title, poster, sources, tracks }`, not the live child element. `sources` contains
fresh `{ src, type, media }` records for the child's direct `src` and `<source>` declarations;
`tracks` contains fresh `{ src, kind, srclang, label, default }` records. A listener that needs to
annotate or reshape the payload must create its own mutable copy; the dispatched detail and every
nested record/array reject mutation.

**Slot:** the default slot accepts direct `<lr-video>` children. Nested videos and other elements
are not playlist items.

**CSS parts:** `base` and `video-playlist` (aliases on the same root node), `playlist`,
`playlist-duration`, `playlist-item`, `playlist-thumbnail`, and `playlist-title`.

**Themeable custom properties:** `--lr-video-playlist-item-current-border-color` (default
`var(--lr-color-brand)`) and `--lr-video-playlist-item-current-background` (default
`var(--lr-color-brand-fill-quiet)`) style the active playlist row.

Only the active child is visible and loaded. Before another child is activated, the outgoing native
player is synchronously paused, stripped of its private source/track clones, and reloaded into an
empty selection state. The incoming child then safely re-clones its own light-DOM declarations.
Valid user volume, mute, playback-rate, and selected-caption preferences carry across that boundary;
selected caption tracks use `hidden`, not `showing`, so the active child's custom overlay is the only
caption paint. Current time does not carry. Events and rejected play promises from a superseded
activation cannot affect the current child. The playlist snapshots each child's authored
`controls`, `iconLibrary`, and `hidden` values before projecting effective state, and restores those
values plus resource selection when that exact child is removed or reparented. Removing or
reordering duplicate-metadata children is identity-safe; disconnecting pauses/unloads every child
while preserving ownership for a later reconnect in a new realm.

Every enabled playlist button has `tabindex="0"`; disabled rows for inert children use `-1`.
Up/Down, Home/End, and mirrored Left/Right remain optional shortcuts, and the selected item exposes
`aria-current`. Each visible known duration is associated with its row using `aria-describedby`;
missing duration creates no empty description, and metadata updates replace the localized value.
At narrow
allocations the sidebar moves below the video through a container query; long titles ellipsize
without widening the host.

**A child marked `inert` is unavailable:** it never becomes the active video,
`next()`/`previous()`/`goTo()` and auto-advance step past it, and its playlist row renders `disabled`
and `tabindex="-1"` so neither sequential nor optional-arrow focus can land on it — an inert element
refuses focus, which would leave `focus()` a silent no-op and kill the next arrow press. `<lr-video>`
has no `disabled`
property; use the platform `inert` state exclusively. Only the child's **own** `inert` counts: a
playlist inerted wholesale by an open modal keeps playing. The attribute is watched live, so
marking the _current_ video inert moves the selection to the nearest enabled child (emitting
`lr-video-change`) and hands optional-arrow focus to the row that replaced it, instead of leaving a
stale arrow-navigation cursor on a row that can no longer take focus.

Only the native `ended` notification drives `autoAdvance`/repeat completion. A native `error`
records the stopped state but never changes selection; recovery and retry remain consumer-owned.

```html
<lr-video-playlist controls="full" repeat="all">
  <lr-video title="Introduction" poster="/posters/introduction.jpg">
    <source src="/video/introduction.mp4" type="video/mp4" />
  </lr-video>
  <lr-video title="Advanced workflow" poster="/posters/advanced.jpg">
    <source src="/video/advanced.mp4" type="video/mp4" />
    <track
      src="/captions/advanced-en.vtt"
      kind="captions"
      srclang="en"
      label="English"
      default
    />
  </lr-video>
</lr-video-playlist>
```
