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

# `lr-tree-item`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tree-item.js';` (stable tag alias; registers the tag)
- **Class** `LyraTreeItem`, also available unregistered from `@aceshooting/lyra-ui/components/data/tree/tree-item.class.js`
- **Family** `components/data/` — see `llms/index.md` for its siblings
- **Status** `stable` 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)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 28 parts, 26 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Documented with** `lr-tree` (same section below)
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-tree` / `lr-tree-item`

An expand/collapse hierarchy (document/graph navigation tree). Mirrors `wa-tree`/`wa-tree-item` and
`sl-tree`/`sl-tree-item`.

An otherwise unnamed data row receives its stable `id` as a semantic name. Usable `accessibleLabel`,
visible label, description, badge content and explicit host naming retain precedence; the fallback
changes neither visible content nor installed data. Blank visible labels remain supported.
Declarative items receive no automatic ID fallback.

**Renamed in 8.0.0 — breaking:** the child element is `<lr-tree-item>` (class `LyraTreeItem`), not
`<lr-tree-node>`/`LyraTreeNode`. It was the only child element in the library whose tag diverged
from both upstreams, so `wa-tree-item`/`sl-tree-item` markup had nothing to rename to. The shared
data types moved to `tree-types.ts` to free the name and are still re-exported from `tree.js`. A
leftover `<lr-tree-node>` is an unregistered tag: it never
upgrades, `<lr-tree>` does not count it as an item at all, and the tree renders its empty state
with the stale markup sitting inert in the light DOM — no error anywhere.

**9.0 type/API cleanup:** rename the structured-data type `TreeItem` to `LyraTreeNodeData`, use
`LyraVariant` for `TreeBadge.tone` instead of `TreeBadgeTone`, and replace a singular `badge` value
with `badges: [{ text: String(value) }]`. The former item-controller fields (`activeId`, `ancestry`,
`depth`, `setSize`, `posInSet`) and context setters were never consumer state; they are now private
owner context maintained by `<lr-tree>`.

**Two child models are accepted, and they never interleave.**

- **Declarative** (mirrors the upstream markup, so a tag rename is the whole migration): write
  nested `<lr-tree-item>` elements as light-DOM children, each carrying its own
  `label`/`expanded`/`disabled`/`selected` attributes. `<lr-tree-item>` moves its nested children
  onto an internal `children` slot itself, so you never write `slot=`.
- **Data**: leave `<lr-tree>` empty and assign `data`, a `LyraTreeNodeData[]` of plain objects. This is this
  library's own original shape and is where per-row icons, secondary descriptions and badges live —
  the declarative model has none of those. `<lr-tree>` creates and reconciles the
  `<lr-tree-item>` children by `id`, and each item renders its own subtree into its own shadow root.
  Every reachable `LyraTreeNodeData.id` must be nonblank and globally unique. Malformed rows and
  later duplicates are omitted before rendering; the first valid depth-first occurrence wins
  and cannot receive focus, selection, expansion, or reorder requests. Supplying unique refreshed
  data releases that fail-closed state.

A tree containing any author-written `<lr-tree-item>` child is read purely as the declarative model
and `data` is ignored. The empty state renders only when neither model has any items.

Both child models use the same selection, roving-focus, checkbox-cascade, icon, and lazy-loading
engine. The upstream lifecycle names are normalized to the library-wide `lr-` prefix;
`lr-node-toggle` and `lr-node-select` remain as additive Lyra notifications. In multiple modes the
tree role explicitly exposes `aria-multiselectable="true"` (and explicit `"false"` otherwise),
while each treeitem host is the sole selected/checked/mixed semantic owner; checkbox-shaped chrome
is decorative and cannot duplicate the row's accessible name.

### `lr-tree`

Implements the full WAI-ARIA treeitem keyboard pattern: a single owner-controlled roving `tabindex`
across every reachable `<lr-tree-item>` and
ArrowUp/Down/Right/Left/Home/End/Enter/Space handled by one delegated `keydown` listener (native
`KeyboardEvent`s are `composed: true` and bubble across shadow-DOM boundaries, so a press inside a
deeply-nested node's own shadow root still reaches it).

**Properties:**

- `data: readonly LyraTreeNodeData[] = []` (attribute: false) — the object child model; ignored
  while any author-written `<lr-tree-item>` child is present. Assignment installs a detached,
  recursively frozen snapshot: mutate caller data only before assignment, then reassign after
  changes. Normalization accepts at most 1,000 valid nodes and 64 descendant levels, and lazily
  inspects at most 10,000 root/child array positions globally in depth-first order. It never
  invokes caller accessors and exposes `dataTruncated = true` when malformed or over-budget input
  was omitted or the inspected-position ceiling was reached. Collapsed branches do not instantiate descendants; disclosure projects only normalized
  children while `aria-setsize` preserves the declared sibling count. `LyraTreeNodeData` is
  `{ readonly id: string; readonly label: string; readonly children?: readonly LyraTreeNodeData[];
readonly selected?: boolean; readonly disabled?: boolean; readonly lazy?: boolean; readonly
badges?: readonly TreeBadge[]; readonly icon?: unknown; readonly description?: string; readonly
accessibleLabel?: string }`. `TreeBadge` is `{ readonly text: string; readonly tone?:
LyraVariant; readonly label?: string }`. `badges` renders tone-mapped chips in order. A nonempty
  `label` makes that chip a named `img`; when the override is omitted or empty, no generic role or
  redundant `aria-label` is added and the visible `text` contributes to the treeitem name naturally.
  `icon` renders as a decorative leading visual, `description` as secondary visible row text, and
  `accessibleLabel` names the `role="treeitem"` host without changing its visible label. An
  author-supplied host `aria-label` takes precedence by presence and is never overwritten or
  removed by later object refreshes; removing the author attribute restores the current data name.
  `id` is the event, roving-focus, reconciliation, and reorder identity and must be unique across
  the complete reachable hierarchy; malformed rows, blank IDs and later duplicates are omitted as described above
- `selection: 'single'|'multiple'|'leaf'|'leaf-multiple' = 'single'` — self-managed selection for
  both child models. `single` selects one item; `leaf` selects one loaded leaf; `multiple` displays
  checkboxes and cascades through enabled descendants; `leaf-multiple` applies that cascade only
  to leaves. Partially-selected branches expose `indeterminate`
- `label: string = ''` — accessible-name fallback for the tree; `role="tree"` lives on an internal
  `[part="base"]` element. A host `aria-label` takes precedence by attribute presence, including an
  explicit empty string; removing it restores the `label` fallback. External `aria-labelledby`
  idrefs are not forwarded across the shadow boundary.
- `reorderable: boolean = false` (reflected) — opts into keyboard reordering. Unset, no `lr-reorder`
  is ever emitted, Ctrl/Cmd+Arrow behaves exactly like a plain Arrow press, and the internal live
  region is not rendered at all.

**Read-only getters:** `selectedItems: readonly LyraTreeItem[]` returns a new frozen snapshot of
selected item elements in document order, including derived fully-selected parents in either
multiple mode. `dataTruncated: boolean` reports bounded/malformed normalization as described above.

**Keyboard:** ArrowDown/ArrowUp move the roving focus to the next/previous _visible_ node.
ArrowRight expands a collapsed node (focus stays put; a second ArrowRight then steps into the first
child) or moves into an already-expanded node's first child. ArrowLeft collapses an expanded node, or
moves focus to its parent. Home/End jump to the first/last visible node. Enter/Space activate
`select()` on the focused node. While `reorderable`, **Ctrl/Cmd**+ArrowUp/ArrowDown moves the focused
node within its own parent's child list instead of navigating. Ctrl/Cmd rather than Alt: Alt+Arrow is
browser back/forward on Windows and Linux. ArrowUp/ArrowDown are not direction-sensitive, so this
binding is deliberately **not** RTL-swapped — "down" always means later in the sibling list.
If a same-id data refresh disables an expanded branch, that reused branch collapses immediately;
enabled descendants are never left visibly stranded outside this navigation walk.

**`inert` excludes an item and its whole subtree from that walk, exactly as `disabled` does.**
`role="treeitem"` and the roving `tabindex` both live on the `<lr-tree-item>` host itself, so an
inert item literally refuses `focus()` — stepping the roving index onto one would leave focus behind
on `<body>` and kill every later arrow press. Marking the focused item inert therefore moves the
roving stop to the next reachable row instead of stranding it, and the state is observed live
(`attributeFilter: ['selected', 'disabled', 'inert', 'lazy']`). Two deliberate limits:

- **Only inertness _inside_ the tree counts** — the item's own `inert`, or that of an ancestor item
  between it and the `<lr-tree>`. An inert ancestor _outside_ the tree (the page behind an open
  modal) inerts every item uniformly, and excluding them all would empty the walk, null out
  `activeId`, and leave the tree with no `tabindex="0"` stop for anything to restore once the dialog
  closes. That case needs no handling: focus cannot be inside the tree at all.
- **Selection ignores it.** An inert subtree is temporarily non-interactive, not deselected, so
  `selectedItems` and the multiple-mode cascade are unchanged and a modal that inerts the page can
  never silently wipe a tree's selection.

**Methods:** `expandAll(): Promise<void>`, `collapseAll(): Promise<void>` (both iterative, bounded,
and resolved only after the affected rendered item cascade settles).

**Events:** `lr-selection-change` (`detail: { selection }`, where both the detail and selection
snapshot are frozen) and `lr-reorder` (`detail: { nodeId, parentNodeId, fromIndex, toIndex }`, only while `reorderable`).
Like every other event here it is a **request**: `data` is host-owned and is never mutated by this
component, so nothing moves until the host reassigns a reordered `data` — focus then follows the
moved node. The live region likewise announces a completed move only after the rendered sibling
order confirms the exact requested swap. Ignored or rejected requests stay silent, and unrelated
updates do not prematurely discard an asynchronously persisted request. `parentNodeId` is `null` for a
top-level item, and `fromIndex`/`toIndex` are **sibling-scoped
indices**, not positions in the flattened visible list. The move is constrained to one sibling list
and never fires at a subtree boundary, so a reorder can never become a reparent: Ctrl+ArrowDown on
the last child of a subtree is ambiguous (the visually next row is a top-level uncle, so "move down"
could mean either "swap with the next sibling" — there is none — or "reparent up a level"), and
reparenting is a structural edit with no keyboard affordance distinguishing the two. Such a request
is simply not made: no event, no announcement, focus stays put. Item events listed below bubble
through the tree as well.

**Slots:** default — top-level `<lr-tree-item>` elements, each nesting its own children (the
declarative model). Leave it empty and assign `data` for the object model; the same slot then holds
the items `<lr-tree>` generates. `expand-icon` and `collapse-icon` provide tree-wide disclosure
icons; an item-level slot with the same name takes precedence.

**CSS parts:** `base` and `tree` are aliases on the same `role="tree"` root; `empty` is the
empty-state message shown when neither child model has any items.

**Themeable custom properties:** shared tokens `--lr-space-xs`/`-s`, `--lr-color-brand-quiet`,
`--lr-color-text-quiet`, `--lr-color-border`, `--lr-color-text`, `--lr-radius`,
`--lr-focus-ring-*` (row `:focus-visible` ring, driven by `:host(:focus-visible)` since the host
itself is the focusable `role="treeitem"`), plus `--indent-size`, `--indent-guide-color`,
`--indent-guide-offset`, `--indent-guide-style`, and `--indent-guide-width`.

**Optional peer deps:** none.

### `lr-tree-item`

One row of the tree, in either child model. `role="treeitem"` (plus
`aria-expanded`/`aria-level`/`aria-setsize`/`aria-posinset`/`aria-selected` and the roving
`tabindex`, driven by `<lr-tree>`) live on the _host_ element itself, not an internal row `<div>` —
so this node's own nested children (whether rendered in its own shadow root or projected from the
light DOM, as further `role="group"` content) are genuine DOM descendants of the treeitem, matching
the WAI-ARIA treeitem pattern's containment expectation.

**Properties — declarative model** (write these as attributes; `item` data seeds the matching state
when assigned):

- `label: string = ''` — the row's label, used only when nothing is slotted into the default slot
- `disabled: boolean = false` (reflected) — removes the item from roving focus and blocks
  select/toggle activation
- `selected: boolean = false` (reflected) — renders the selected state and is exposed as
  `aria-selected`
- `lazy: boolean = false` (reflected) — defers expansion, renders the spinner, and emits
  `lr-lazy-load` until children arrive or `lazy` is cleared

**Properties — data model:**

- `item?: LyraTreeNodeData` (attribute: false) — the whole subtree as one object, normally assigned by
  `<lr-tree>` from its `data`. An assigned `item` **wins** for label/disabled/children and seeds
  `selected`/`lazy`; a refreshed object identity re-seeds those values. Light-DOM children are
  ignored while `item` is assigned. Outside an owning tree, an omitted `item.selected` leaves
  `aria-selected` off the host; an owning tree always publishes explicit true/false state. Assign
  `undefined` to return safely to the declarative model and reset data-seeded selected/lazy state

**Properties — shared:**

- `expanded: boolean = false` (reflected)
- `loading: boolean` (read-only) — a lazy expansion is waiting for children
- `indeterminate: boolean` (read-only) — an enabled branch has some but not all selectable
  descendants selected

**Read-only getters** — each answers for whichever child model is in use, which is what lets
`<lr-tree>` drive both with one implementation:

- `nodeId: string` — this item's identity: `item.id` in the data model, or a generated per-element
  id in the declarative one (where the markup carries no id of its own). It is the `nodeId` every
  `lr-node-toggle` / `lr-node-select` / `lr-reorder` detail carries, and what the tree tracks its
  roving tabindex by
- `isDisabled: boolean` — `item.disabled` in the data model, the `disabled` property in the
  declarative one
- `nodeLabel: string` — this item's spoken name, used for the tree's reorder announcements: a host
  `aria-label` is authoritative in both models
  (including the component-owned stable-ID fallback for otherwise unnamed data rows); otherwise `item.accessibleLabel || item.label` in
  the data model, or flattened accessibility-visible slotted label text (nested items excluded)
  followed by the `label` fallback in the declarative one. Direct and forwarding-slot
  text/ARIA/visibility mutations update the name
- `hasChildren: boolean` — whether this node has at least one child in whichever model is in use.
  Leaf nodes never expose `aria-expanded` and cannot expand or collapse. It also reports `false`
  past the owner controller's 64-level bound, which stops runaway recursion; cyclic object graphs
  are detected by private ancestry identity

**Methods:** `expand()`, `collapse()` (each a no-op if already in that state, disabled, loading, or a leaf),
`select()` (fires `lr-node-select`; a no-op while disabled), and host `click()` (forwards exactly
once to the same selection path and is likewise disabled-gated). `childItems(): LyraTreeItem[]` returns
this node's **direct** child `<lr-tree-item>` elements — from its own shadow root in the data model,
from its own light-DOM children in the declarative one. A grandchild is not included; it lives under
its own parent. `getChildrenItems({ includeDisabled = true } = {})` is the upstream-compatible
public spelling over the same direct-child list.

**Events:** `lr-node-toggle` (`detail: { nodeId, expanded }`, fired by `expand()`/`collapse()` — via
the toggle button or ArrowRight/ArrowLeft), `lr-node-select` (`detail: { nodeId }`, fired by `select()`
— via clicking anywhere in the row or Enter/Space) — dispatched from `lr-tree-item`,
bubble/compose up through `lr-tree`'s light DOM. `lr-expand`/`lr-collapse` fire when a transition
begins; `lr-after-expand`/`lr-after-collapse` fire after the matching themeable duration. Rapid
opposite transitions and disconnects invalidate stale after-events. Collapse keeps the subtree
mounted through its real opacity animation and removes it only when the animation completes;
reduced motion settles immediately, and duration parsing is finite/nonnegative/timer-capped.
`lr-lazy-load` requests data
with `detail: { item, generation }`; `lr-lazy-change` reports `detail: { item, loading }` when the
pending state starts or ends. Disabling or disconnecting an item invalidates the pending generation.

**Slots:** default — the row's label content in the declarative model. `children` — where nested
`<lr-tree-item>` children are projected; **assigned by the component**, so write the nested items in
the default slot and never set `slot="children"` yourself. `expand-icon` and `collapse-icon`
override the owning tree's corresponding icon for one item. The label/children slots are unused in
the data model.

Visual slot selection is separate from `nodeLabel` extraction: flattened element-only and visible
`aria-hidden` content still chooses the authored slot instead of the `label` fallback, while hidden
content is omitted from the spoken name. Host `aria-label` remains authoritative by presence.

**CSS parts:** `base` and `tree-item` are aliases on the same outer wrapper around the row and child
group; `row`, `toggle`, `icon`, `content`, `label`, `description`, `badge`, `group`, `item`,
`item--disabled`, `item--expanded`, `item--indeterminate`, `item--selected`, `indentation`,
`expand-button`, `spinner`, `spinner__base`, `children`, `checkbox`, `checkbox__base`,
`checkbox__control`, `checkbox__control--checked`, `checkbox__control--indeterminate`,
`checkbox__checked-icon`, `checkbox__indeterminate-icon`, and `checkbox__label`. `badge`
is applied to every `item.badges` chip; chips carry
`data-tone="neutral|brand|success|warning|danger"`. `icon` is `aria-hidden="true"`; `content`
groups the primary label and optional wrapping secondary description while preserving one
interactive treeitem per row. `icon`, `description` and `badge` render only in the data model —
the declarative model has no icon/description/badge inputs, so a row written as markup renders
`row`/`toggle`/`content`/`label` (and `group` while expanded) and nothing else.
`item` and its four state aliases are real painted row containers: consumer background, border,
padding, and opacity rules reach the visible row rather than a boxless wrapper.
The enabled disclosure `toggle` has its own pointer-hover feedback in addition to the row's hover
treatment; disabled toggles remain visually inert.
In the data model, every recursively rendered child forwards this complete part list under the
same names, so one selector on the outer item, such as `lr-tree-item::part(row)`, reaches matching
parts at every rendered depth. Declarative children remain light-DOM hosts and can be matched
directly as `<lr-tree-item>` elements.

**Themeable custom properties:** `--indent-size` (default `var(--lr-space-l)`, applied once per
nesting depth), `--indent-guide-color` (default `var(--lr-color-border)`),
`--indent-guide-offset` (default `0`, the guide's block-axis inset at both ends),
`--indent-guide-style` (default `solid`), and `--indent-guide-width` (default `0`); these mirrored
properties are consumed directly by every `<lr-tree-item>` and may be set on an item or inherited
from `<lr-tree>`. `--lr-tree-depth` is internal and set inline per row for indentation;
`--show-duration`/`--hide-duration` both default through `--lr-duration-base`;
`--lr-tree-selected-color` and `--lr-tree-selected-bg` for the selected row; and paired
`--lr-tree-checkbox-checked-border-color`, `--lr-tree-checkbox-checked-bg`,
`--lr-tree-checkbox-checked-color`, `--lr-tree-checkbox-indeterminate-border-color`,
`--lr-tree-checkbox-indeterminate-bg`, and `--lr-tree-checkbox-indeterminate-color` independently
theme the two multiple-selection checkbox states (brand border/background and on-brand glyph
fallbacks); and paired
`--lr-tree-badge-{neutral|brand|success|warning|danger}-color` /
`--lr-tree-badge-{neutral|brand|success|warning|danger}-bg` properties for each badge tone. Each
badge property falls back to its corresponding shared semantic token. The expanded names are
`--lr-tree-badge-neutral-color`, `--lr-tree-badge-neutral-bg`, `--lr-tree-badge-brand-color`,
`--lr-tree-badge-brand-bg`, `--lr-tree-badge-success-color`, `--lr-tree-badge-success-bg`,
`--lr-tree-badge-warning-color`, `--lr-tree-badge-warning-bg`, `--lr-tree-badge-danger-color`,
and `--lr-tree-badge-danger-bg`.

**Optional peer deps:** none.

The data model — icons, descriptions and badges live here:

```html
<lr-tree></lr-tree>
<script>
  document.querySelector("lr-tree").data = [
    {
      id: "1",
      label: "Root",
      description: "Two child documents",
      accessibleLabel: "Root, two child documents",
      icon: document.createTextNode("◇"),
      children: [
        { id: "1a", label: "Child A" },
        { id: "1b", label: "Child B", badges: [{ text: "3" }] },
      ],
    },
  ];
</script>
```

The declarative model — the same shape a renamed `wa-tree`/`sl-tree` subtree lands in, with no
`slot=` anywhere and no `data` assignment:

```html
<lr-tree label="Documents">
  <lr-tree-item label="Root" expanded>
    <lr-tree-item label="Child A"></lr-tree-item>
    <lr-tree-item label="Child B" disabled></lr-tree-item>
  </lr-tree-item>
</lr-tree>
```

**Known gotchas:**

- all four previously-known ARIA gaps in this pair are fixed: the treeitem row is now genuinely
  keyboard-operable with a roving tabindex and full arrow-key navigation (not just the expand/collapse
  button); the expanded-children `role="group"` is now a real DOM descendant of its `role="treeitem"`
  host rather than a shadow-DOM sibling; by-id reconciliation (preserving `expanded` state across
  data reassignment) now applies at every depth via a keyed `repeat()`, not just depth 0; and
  `role="tree"` now has an accessible name via the new `label` property.
- `lr-tree`'s `getUpdateComplete()` cascades through owner-controlled descendants
  `<lr-tree-item>`'s own `updateComplete` (see `update-cascade.ts`) so that code awaiting the
  tree's `updateComplete` does not run before a reachable nested node has finished rendering its
  roving `tabIndex`. The same 64-level/1,000-node work bounds apply to context, focus, selection,
  and disclosure walks.
- row enrichment is intentionally structured rather than an unrestricted renderer: use `icon`,
  `label`, `description`, `badges`, and `accessibleLabel`. This keeps the host as the single
  `role="treeitem"` interaction target and preserves the APG keyboard model.
- `lr-file-tree` does **not** forward `reorderable`, and deliberately so: its `LyraTreeNodeData[]` is derived
  from `nodes` on every render and keyed by filesystem path, an order it does not own.
- in the declarative model, appending a child while the parent is collapsed still registers: a
  `childList` MutationObserver assigns the `children` slot and requests an update, because
  `slotchange` alone cannot see a child added to a slot that is not currently rendered. Editing a
  label in place is picked up the same way.
- a declarative row whose default slot holds only whitespace around its nested items still shows the
  `label` attribute — indentation does not count as slotted label content, which is what makes the
  common `<lr-tree-item label="…">` + nested-children shape render its label.

---
