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

# `lr-lightbox`

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

A modal, full-screen image viewer with prev/next navigation, captions, and pan/zoom delegated to one
stable embedded `<lr-pan-zoom>`. It renders its own dialog panel (not a nested `<lr-dialog>`)
but shares the same overlay infrastructure as `<lr-dialog>`/`<lr-command-palette>` — stacking, focus
trap, Escape/backdrop dismissal, scroll lock, and focus return.

**Properties:**

- `open: boolean = false` (reflected) — post-render writes run the same cancelable lifecycle as
  `show()`/`hide()`/`close()`. Initial `open` markup is state and emits no lifecycle events.
- `images: readonly LyraLightboxImage[] = []` (attribute: false) —
  `LyraLightboxImage { readonly src: string; readonly alt?: string; readonly caption?: string }`.
  Assignment clones/freezes the records and inspects at most 10,000 candidates; malformed records
  are omitted and updates require a new collection assignment. `src` is passed to the embedded
  frame, which runs it through `safeMediaSrc()`. `alt`/`caption` are caller data, never localized.
- `index: number = 0` (reflected) — clamped defensively for rendering and silently re-synced (no
  event) when `images` shrinks.
- `loop: boolean = false` (reflected) — wraps prev/next past the ends.
- `lightDismiss: boolean = false` (attribute `light-dismiss`) — opt in to backdrop dismissal. Off by default, matching `lr-dialog`.
- `showCounter: boolean = true` (attribute `show-counter`, **not reflected**) — shows the visible
  `[part="counter"]`. `show-counter="false"` clears it from plain HTML (the attribute is parsed by
  literal string, not by presence, so a true-defaulting boolean can actually be turned off), and a
  `.showCounter=${false}` property binding does the same. Nothing is ever written back to the
  attribute — no stylesheet or selector keys off `[show-counter]`.
  Spoken position updates remain active when the counter is hidden: the shadow
  `[part="live-region"]` is only an `aria-hidden` text mirror, while announcements append to the
  shared light-DOM polite sink. Announcements stay silent when the lightbox or a composed ancestor
  is excluded from the accessibility tree.
- `minZoom: number = 0.5`, `maxZoom: number = 4`, `zoomStep: number = 0.25` (attributes `min-zoom`/
  `max-zoom`/`zoom-step`) — pure pass-throughs to the embedded `<lr-pan-zoom>`, which does the
  normalizing.
- `fit: LyraImageFit = 'actual'` (reflected) — opt-in base sizing policy for the current image,
  reusing `<lr-image-viewer>`'s `contain`/`width`/`actual` vocabulary. The default preserves the
  existing natural-size behavior. `contain` fits both landscape and tall images inside the stage;
  `width` fills its inline size. CSS recalculates either policy when the stage is resized or a
  delayed image source finishes loading, so navigation and opening do not require consumer timers.
  Fit is the base zoom policy: an explicit user zoom remains until navigation or a reset, and
  changing the theme does not mutate the lightbox's zoom property.
- `accessibleLabel: string | null = null` (attribute `aria-label`) — the panel's accessible name,
  overriding the localized `lightboxLabel`.

**Methods:** promise-based `show()`, `hide()`, and `close(reason?)`, plus `next()`, `previous()`, and
`goTo(index)`. `show()`/`hide()` resolve after the successful rendered transition; a veto resolves
without changing state. `goTo()` ignores a non-finite
index without changing state or emitting `lr-index-change`. A finite fractional index is truncated
toward zero before clamping or loop wrapping, so `lr-index-change.detail.index` is always the
actual rendered integer index; `reason` defaults to `'api'` and is forwarded as the close event's
detail.

**Events:** cancelable `lr-show`, followed after a successful open render by `lr-after-show`;
cancelable `lr-hide` (`detail: LyraLightboxHideDetail = { source: Element }`), then
`lr-lightbox-close` (`detail: LyraLightboxCloseReason = 'escape' | 'backdrop' |
'close-button' | 'api' | 'unmount' | (string & {})`; **cancelable** — `preventDefault()` blocks
closing on every path, including a consumer's own `close()` call), followed after a successful
closed render by `lr-after-hide`. Removal while open emits the settled non-vetoable
hide/close/after-hide order with reason `'unmount'`. `lr-index-change` (`detail: { index }`, fired
only for internally-driven navigation — a button, a keyboard shortcut, or `next()`/`previous()`/
`goTo()`; **not** when a consumer sets `index`/`images` directly); `lr-zoom-change` (`detail: {
zoom }`) is not emitted by the lightbox itself — it bubbles up composed from the embedded frame.

**Slots:** `actions` — extra toolbar buttons (download/share/delete), rendered in `[part="toolbar"]`
between the counter and the close button.

**CSS parts:** `backdrop`, `panel` (`role="dialog"` + `aria-modal="true"` while open; fills the
padded safe area rather than shrink-wrapping), `toolbar`, `counter` (visible localized "Image N of
Total"), `live-region` (an `aria-hidden` shadow text mirror; each post-mount `index` change,
including a consumer-driven one, appends the localized position to the document's shared
`[data-lr-live-region="polite"]` sink; initial mount and reconnect are silent), `actions` (wrapper,
`hidden` when nothing is slotted),
`close-button` (always rendered — unlike `<lr-dialog>`'s opt-in `closable`), `stage`, `frame` (the
embedded `<lr-pan-zoom>`), forwarded aliases `frame-viewport`, `frame-content`, `frame-controls`,
`previous-button`,
`previous-glyph`, `next-button`, `next-glyph`, `caption` (only when the current image has one; its
`id` is the panel's `aria-describedby` target).

**Themeable custom properties:** `--lr-lightbox-overlay-color` (default
`var(--lr-color-overlay-strong)` — the backdrop scrim), `--lr-lightbox-control-bg` (default
`var(--lr-color-neutral)`) and `--lr-lightbox-control-color` (default `var(--lr-color-on-neutral)`)
— background/foreground for the counter, caption, and every floating/toolbar icon button; these use
the solid neutral token rather than `--lr-color-surface` because the controls float over arbitrary
photo content.

**Optional peer deps:** none.

**Known gotchas:**

- keyboard navigation is RTL-aware on panel/chrome: Arrow forward/back (mirrored under `rtl`),
  `Home`, `End`. When the embedded pan/zoom viewport owns focus, those keys remain with its native
  scroll surface and never change gallery item; `+`/`-`/`0` remain its zoom shortcuts.
- initial focus deliberately goes to `close-button`, not the first tabbable element — a slotted
  `actions` button placed before it does not steal focus.
- zoom/pan reset on navigation is imperative (`resetView()` from `updated()`), not a binding; the
  frame element is reused across navigations rather than recreated, so a keyboard user who tabbed
  into the viewport keeps focus.
- fit sizing is CSS-driven inside the stable frame. The rendered DOM image is not copied or
  measured by the lightbox, so stale load events cannot apply a previous image's size. The fit
  policy covers the painted image only; the lightbox's DOM caption, toolbar, legend-like actions,
  and tooltip content remain outside that image surface.
- scope for v1: no per-image slotted content (data-driven via `images` only), no dot indicators, no
  visual open/close animation, no click-image-to-navigate, no touch-swipe. Lifecycle phases are
  still observable through the before/after events and methods above.
