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

# `lr-json-viewer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-json-viewer.js';` (stable tag alias; registers the tag)
- **Class** `LyraJsonViewer`, also available unregistered from `@aceshooting/lyra-ui/components/utility/json-viewer/json-viewer.class.js`
- **Family** `components/utility/` — 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** 10 parts, 9 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-json-viewer`

A collapsible, copyable tree view for an arbitrary JSON-serializable value (object, array, string,
number, boolean, null, or `undefined`). Serves as a fallback renderer wherever a raw payload needs
inspecting without a bespoke view. Assignment creates one bounded, frozen graph from own enumerable
data descriptors. It never invokes getters or object-conversion hooks; accessor/reflection-failed
branches and function or symbol leaves are omitted, while bigint is retained as decimal text.
Rendering, searching, toolbar copy, and per-node copy all use that same owned graph, so later
mutation or revocation of the supplied object cannot change a displayed or copied value. Ordinary
aliases, cycles, and sparse-array holes are retained. Expand/collapse state is keyed by structural path (not object
identity), so it survives a `data` reassignment that keeps the same shape — e.g. a streaming result
being patched in place. A container value that self-references (directly or through a longer cycle)
renders as a leaf `Circular reference` marker (`data-type="circular"`) instead of recursing — no
stack overflow on cyclic `data`.

Removing `search` clears matches while retaining null property readback. Before any match is active,
`searchPrevious()` selects the final match; later next/previous navigation wraps. Navigation may
reopen the selected match’s ancestors while unrelated manual collapse remains intact.

**Properties:**

- `data: unknown` (attribute `false` — property-only, not settable via an HTML attribute)
- `collapsedDepth?: number` (attribute `collapsed-depth`) — nodes at or beyond this nesting depth
  (root = `0`) start collapsed; omitted/`undefined` means nothing auto-collapses
- `maxHeight: string = ''` (attribute `max-height`) — a CSS length (e.g. `"20rem"`); once set, the
  viewer scrolls internally past this height instead of growing the page. Values that do not parse
  as CSS `max-height`, contain declaration breaks, or contain `url()` are ignored, leaving
  `--lr-json-viewer-max-height` in control
- `copyable: boolean = false` (reflected) — shows copy-to-clipboard affordances: one for the whole
  value, plus one per node
- `search: string = ''` — case-insensitive substring match against keys/values; matches are
  highlighted and their ancestors auto-expanded

**Methods:** `runSearch(query)` sets the declarative `search` property and awaits the recompute,
resolving the match count — named distinctly from `search` because a class member can't share a name
with a reactive property. `searchNext()`/`searchPrevious()` advance/step back a match cursor
(wrapping), reveal that selected match even when one of its ancestors was explicitly collapsed,
mark it as the active `aria-current` result, announce its position, and scroll it into view;
they resolve `false` when there are no matches. `clearSearch()` resets `search` to `''`, clearing all
matches and the cursor.

**Events:** `lr-copy` (`detail: LyraClipboardWriteSuccess`, `{ ok: true; text: string }`) — fired by
the top-level copy button or a per-node one only after the owning browsing context's clipboard write
fulfills. `lr-error` (no detail) and `lr-copy-error` (`detail: LyraClipboardWriteFailure`) fire when
serialization or clipboard writing fails; the detailed frozen outcome carries `ok: false`, the
attempted text, a reason of `'unsupported' | 'denied' | 'failed'`, and the original error. Failures
announce localized `copyFailed`; the raw platform error is never rendered. Copying a circular
`data` value serializes safely, substituting the same `Circular reference` marker the tree view
renders, instead of throwing. `lr-search-change`
(`detail: { query, matchCount, matchCountExact, activeIndex }`) —
fired whenever the search query, match count, or active-match cursor changes, from
`runSearch()`/`searchNext()`/`searchPrevious()`/`clearSearch()`, or a direct `search`/`data`
property write. `matchCountExact` is `false` when the bounded traversal only proves a lower bound;
the rendered count uses an “at least” prefix in the same case.

**Slots:** none — the tree is rendered entirely from `data`.

**CSS parts:** `base` (root scroll container, respects `max-height`), `toolbar` (wrapper around the
top-level copy button, only rendered when `copyable`), `tree` (wrapper around the rendered node
tree; a host `aria-label` is forwarded here), `row` (every structural opening/value and
closing-delimiter row), `key` (an object property key or array index label, `data-match` while it matches `search`,
`data-active` while it is the current `searchNext()`/`searchPrevious()` cursor position),
`value` (a primitive value's text — carries `data-type` of
`string`/`number`/`boolean`/`null`/`undefined`/`circular` for per-type coloring, `data-match`
while it matches `search`, and `data-active` while it is the current cursor position), `bracket` (a
`{`, `}`, `[`, or `]` delimiter), `toggle` (a container node's expand/collapse button; hidden but
present for row alignment on leaf/empty nodes),
`copy-button` (a copy-to-clipboard button — the top-level one in `toolbar` (aria-label "Copy JSON to
clipboard") or a per-node one (aria-label `Copy ${key/type}`, e.g. "Copy age"); only rendered when
`copyable`), `limit` (the localized notice rendered below the tree when the depth/node traversal
budget truncates rendering or search — absent entirely for any document within budget)

Collapsed arrays retain their exact safe `.length`. A broad object whose key count itself exceeds
the traversal budget uses an “at least” preview rather than presenting the retained prefix as a
false total. Per-row copy actions remain visible by default on coarse-pointer/no-hover devices;
fine-pointer users retain the hover/focus reveal.

Active-match position changes are appended to Lyra's shared light-DOM polite announcement sink.
The shadow tree keeps only an `aria-hidden` text mirror, so the same result is not announced twice;
initial connection and reconnection—including a detached cursor update whose render settles during
reattachment—do not replay the current cursor. Search navigation while the viewer or a composed
ancestor is accessibility-hidden also stays silent. After cross-document adoption, smooth-scroll
motion preferences and best-effort clipboard writes use the viewer's current owner window.

**Themeable custom properties:** `--lr-json-viewer-max-height` (default `none` — grows with content
until `max-height` is set), `--lr-json-viewer-font` (default `var(--lr-font-mono)`),
`--lr-json-viewer-match-bg` (default `var(--lr-color-warning-quiet)`) — background, and surrounding
box-shadow, of a key/value that currently matches `search`. Component-scoped indirection over the
shared `--lr-color-warning-quiet` token, so a consumer can retheme just this search-match highlight
without repainting every other warning-toned surface that reads the same shared token;
`--lr-json-viewer-row-hover-bg` (default `var(--lr-color-brand-quiet)`) — structural-row hover
background;
`--lr-json-viewer-active-outline` (default `var(--lr-focus-ring-color)`) — outline color for the
current imperative search match; `--lr-json-viewer-string-color` (default
`var(--lr-color-success)`), `--lr-json-viewer-number-color` (default `var(--lr-color-brand)`),
`--lr-json-viewer-boolean-color` (default `var(--lr-color-warning)`), and
`--lr-json-viewer-null-color` (default `var(--lr-color-text-quiet)`) — per-value-type color hooks.
Plus shared
tokens `--lr-color-border/-surface/-text/-text-quiet/-brand/-brand-quiet/-success/-warning/-warning-quiet`,
`--lr-radius`, `--lr-space-xs/-s/-l`, `--lr-focus-ring-width/-color/-offset`,
`--lr-transition-fast`.

**Optional peer deps:** none.

```ts
import { html } from "lit";
import "@aceshooting/lyra-ui/components/utility/json-viewer/json-viewer.js";

html`<lr-json-viewer
  .data=${apiResponse}
  copyable
  max-height="24rem"
  search=${query}
></lr-json-viewer>`;
```

```html
<lr-json-viewer copyable max-height="24rem"></lr-json-viewer>
<script type="module">
  document.querySelector("lr-json-viewer").data = {
    hello: "world",
    items: [1, 2, 3],
  };
</script>
```

**Known gotchas:**

- `data` is property-only (`attribute: false`) — it must be set via `.data = ...` or a lit-html `.data=${...}`
  binding, never as a plain HTML attribute.
- Search highlighting auto-expands only the _ancestors_ of a match, not the whole tree — a
  non-matching sibling subtree elsewhere stays collapsed (or expanded) exactly as it already was.
- An explicit per-node expand/collapse (from clicking a node's `toggle` button) overrides
  `collapsedDepth` and declarative search-driven auto-expansion for that path. Imperative
  `searchNext()`/`searchPrevious()` navigation may reopen the ancestors of the selected result so
  the active match is never hidden; otherwise the override persists until `data` is reassigned with
  a different shape.
- Per-node copy buttons call `stopPropagation()` on click so clicking one doesn't also toggle the
  row's expand/collapse state.
- Whole-value copy always produces text, including for a root `undefined`, `Symbol`, or function
  that native `JSON.stringify()` would otherwise return as no value.

---
