Skip to content

Listbox — live demo ​

This is the real @rozie-ui/listbox-vue package running on this page (VitePress is itself a Vue app) — built from this repo's workspace, not npm: @rozie-ui/listbox-vue has not published yet, only @rozie-ui/listbox-solid has debuted so far. Open the select with the keyboard or mouse, type to filter the combobox, toggle options in the multi-select — then watch the two-way bound value update. The same Listbox component, with the same API, ships for React, Vue, Svelte, Angular, Solid, and Lit. It needs no engine and no required CSS — the ARIA behaviour and a tokenised skin ship inside the component.

value is two-way bound with v-model:value — the readout updates the instant you commit a selection, and a consumer write flows back in. The Select instance's buttons drive the imperative handle (open(), clear()) grabbed through Vue's ref. Flip combobox for a filterable text input, add multiple for array values — the same component, the same surface. See the full API for every prop, event, slot, and handle verb, plus theming and keyboard reference.

What ships for each framework ​

You author the component once as a .rozie file:

html
<!--
  Listbox.rozie — a headless, WAI-ARIA select-only Listbox.

  The first @rozie-ui family with NO third-party vanilla engine: every line of
  behaviour — roving virtual focus (aria-activedescendant), full keyboard
  navigation, type-ahead, single + multi select — is authored in Rozie itself.
  (Type-to-filter is owned by the sibling @rozie-ui/combobox family — P3/D-03
  retired Listbox's editable-input mode; both families share the
  @rozie-ui/headless-core/listCore.rzts spine.) It exists to stress the *native*
  author-side primitives the engine-wrapper families never exercise on their own:

    - $computed-derived state          → the filtered + active-resolved option list
    - parameterized @keydown modifiers → arrow/home/end/enter/escape navigation
    - $refs-driven focus management     → input/listbox/option focus, read post-mount only
    - two-way r-model:value            → selection is a controlled form value (Angular CVA)
    - scoped slots                     → #option / #selected / #empty render props
    - $expose imperative handle        → open / close / toggle / clear / focusControl

  It follows the ARIA APG "Select-Only Combobox" pattern (the trigger keeps
  role="combobox"): DOM focus stays on the control, the highlighted option is
  tracked virtually via `aria-activedescendant`, and selection commits a fresh value.

  Authoring notes (collision classes a no-engine component is the first to hit):
    - `$data.open` collides with the `open` $expose verb — Phase 46 ITEM-5 now
      auto-renames the INTERNAL state to `open$local` (cross-target; the exposed
      `open()` verb stays). This file dogfoods that fix by using the natural
      `$data.open` instead of the old `$data.expanded` workaround.
    - A local helper named `valueOf` (an Object.prototype member) collides with
      the inherited member when it becomes a Lit/Angular class field — Phase 46
      ITEM-5 now auto-renames it to `valueOf$local`. This file dogfoods that fix
      by using the natural `valueOf` instead of the old `valueOf` workaround.
    - The focus verb is `focusControl`, not `focus` — a `focus` $expose verb is a
      DELIBERATE override of the inherited HTMLElement.focus on the Lit custom
      element (ROZ137 warns, does not auto-rename — the public handle is intended).
    - `open`/`close`/`toggle` (and `select`/`clear`) each `$emit` directly in
      `listCore.rzts` — Phase 73 item #8 removed the old "route every emit
      through ONE wrapper fn" workaround after verifying against the current
      emitter that the shipped Phase 46 ITEM-1 hoist-once dedupe already
      collapses N escaping helpers sharing an emit target into a single
      `const {onX}=props` destructure (no TS2451), regardless of how many
      functions call `$emit` for the same event.

  Consumer example (select-only):

    <Listbox r-model:value="$data.fruit" :options="$data.fruits" placeholder="Pick a fruit…">
      <template #option="{ option, active, selected }">
        <span :class="{ active, selected }">{{ option.label }}</span>
      </template>
    </Listbox>
-->

<rozie name="Listbox" vendorable="true">

<props>
{
  // The option set. Each entry is either a primitive (string/number) or an
  // object; objects resolve their label/value/disabled via the option* props
  // (falling back to `.label` / `.value` / `.disabled`).
  options:        {
    type: Array,
    default: () => [],
    docs: {
      description:
        'The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.',
    },
  },

  // The selected value (two-way). Scalar in single-select; an array of values
  // in multi-select. As the sole `model:true` prop it drives the Angular CVA —
  // a Listbox IS a form control.
  value:          {
    type: null,
    default: null,
    model: true,
    docs: {
      description:
        'The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).',
      example: '<Listbox r-model:value="fruit" :options="fruits" />',
    },
  },

  // Multi-select: `value` becomes an array; selecting toggles membership and
  // keeps the popup open.
  multiple:       {
    type: Boolean,
    default: false,
    docs: {
      description:
        'Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.',
    },
  },

  // Render the results list IN FLOW (static) instead of as an absolute popup.
  // Use when the listbox is embedded inside a fixed-size, overflow-clipped
  // container (e.g. a command-palette panel) where an absolute popup would be
  // culled. Off (default) keeps the standalone dropdown behavior unchanged.
  inline:         {
    type: Boolean,
    default: false,
    docs: {
      description:
        'Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).',
    },
  },

  disabled:       {
    type: Boolean,
    default: false,
    docs: {
      description:
        'Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.',
    },
  },
  placeholder:    {
    type: String,
    default: '',
    docs: {
      description: 'Placeholder text shown in the empty control.',
    },
  },

  // Close the popup after a selection. Defaults true; multi-select callers
  // usually want it open — pass :closeOnSelect="false".
  closeOnSelect:  {
    type: Boolean,
    default: true,
    docs: {
      description:
        'Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.',
    },
  },

  // Resolver overrides for object options.
  optionLabel:    {
    type: Function,
    default: null,
    docs: {
      description:
        "Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.",
    },
  },
  optionValue:    {
    type: Function,
    default: null,
    docs: {
      description:
        "Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.",
    },
  },
  optionDisabled: {
    type: Function,
    default: null,
    docs: {
      description:
        'Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option\'s `.disabled` property.',
    },
  },

  // Stable id base for the ARIA wiring (listbox id + per-option ids +
  // aria-activedescendant). '' (the default) → a unique per-instance base
  // generated in $onMount (idRoot()), so instances never share ids.
  id:             {
    type: String,
    default: '',
    docs: {
      description:
        'Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.',
    },
  },

  // Accessible name for the control when there is no visible <label for>.
  ariaLabel:      {
    type: String,
    default: null,
    docs: {
      description:
        'Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).',
    },
  },

  // ── Vertical option windowing (Phase 64 P4, SC-5) ──────────────────────
  // Opt-in long-list windowing. When `true`, only the visible slice of options
  // renders inside the bounded, scrolling list (with leading/trailing spacer
  // rows preserving total scroll height), windowing over the FILTERED option set
  // via the shared @rozie-ui/headless-core/windowing.rzts math (the same
  // virtual-core bridge data-table uses, wired here per-consumer with a no-op pin
  // hook). Default `false` is byte-identical to a non-windowed listbox.
  virtual:        {
    type: Boolean,
    default: false,
    docs: {
      description:
        'Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.',
    },
  },

  // Estimated option row height (px) seeding the windowing engine before
  // measureElement refines actual heights. Only consulted when `virtual` is on.
  estimateRowHeight: {
    type: Number,
    default: 36,
    docs: {
      description:
        'Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.',
    },
  },

  // A CSS length string bounding the list scroll container when `virtual` is on
  // (e.g. '320px'). Mirrored to the --rozie-listbox-max-height token.
  maxHeight:      {
    type: String,
    default: '',
    docs: {
      description:
        "A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.",
    },
  },
}
</props>

<data>
{
  // The generated id base (idRoot()), set once in $onMount when `id` is empty.
  autoId: '',
  open: false,
  // Virtual focus: index into the visible option list, or -1.
  activeIndex: -1,
  // Kept at '' for select-only: the shared spine's `visibleOptions` reads it (the
  // identity path when empty) and the `#empty` slot exposes it. No <input> writes
  // it now that the editable-input mode is retired (P3/D-03).
  query: '',
  // ── Windowing host state (Phase 64 P4) — the windowing.rzts host contract ──
  // `rows` is the indexable full model the windowing math maps the slice over
  // (kept === windowSource() / visibleOptions()); windowVer/editVer are the
  // window/edit version reactivity bumps (editVer is inert here — the no-op pin
  // hook never bumps it — but the shared math reads it, so it must exist).
  rows: [],
  windowVer: 0,
  editVer: 0,
}
</data>

<script>
// Type-ahead buffer for the select-only listbox trigger. Module-scope
// `let`s reassigned from handlers → the React emitter hoists them to `useRef`
// so they persist across renders (the setup-once guarantee); no-op elsewhere.
// They STAY in this host (not the shared spine) per the A==B rule: reassigned
// module-`let`s + sigils live in the host; the partial only closes over them.
let typeBuffer = ''
let typeTimer = null

// ---- shared list spine (P2: @rozie-ui/headless-core/listCore.rzts) ------
// The option resolvers, client filter, enabled-index navigation, the keyboard
// reducer, type-ahead, single+multi selection, open/close state, and
// activeDescendant derivation now live in the shared, focus-/input-mode
// parameterized list spine. It is a compile-time `.rzts` script-partial: it
// dissolves into this leaf via inlineScriptPartials() before IR lowering (zero
// runtime dep). Listbox consumes it in focus-model `activedescendant` +
// input-mode `select-only` + multi + type-ahead. The spine closes over this
// host's pieces by convention: the reassigned module-`let`s typeBuffer/typeTimer
// (above) and the impure ref fns focusControl/scrollActiveIntoView (below).
import { labelOf, valueOf, disabledOf, optionId, visibleOptions, selectedLabel, activeDescendant, isSelected, resolveInitialActive, open, close, toggle, select, clear, nextEnabled, move, moveEdge, commitActive, onTypeahead, onControlKeyDown, onOptionPointerMove } from '@rozie-ui/headless-core/listCore.rzts'

// ---- shared windowing math (Phase 64 P4: @rozie-ui/headless-core/windowing.rzts) ----
// The PURE windowing math (the windowed slice + spacer geometry) is the SAME
// virtual-core bridge data-table consumes. It is a compile-time `.rzts` partial:
// the imported symbols DISSOLVE into this leaf via inlineScriptPartials() before
// IR lowering (zero runtime dep). The math closes over this host's pieces BY
// CONVENTION (D-04/D-05): windowSource(), the virtualizer instance + gridScrollEl +
// the virtual-core fns, scheduleRemeasure(), $data.windowVer/$data.editVer, and the
// no-op pin hook pinnedEditIndex()/pinnedMeasurement() defined below. The impure
// DOM/refs pieces stay HERE per-consumer (ROZ123). NB the pin hooks are defined in
// THIS host (not the shared partial) so data-table's A==B byte-identity is untouched.
import { virtualItemKey, virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow } from '@rozie-ui/headless-core/windowing.rzts'

// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core'

// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.
let virtualizer = null
let virtualizerCleanup = null
let gridScrollEl = null
let remeasurePending = false
// Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
// the option count at that moment, and the last scrollTop already accounted for.
let scrollEndPinned = false
let scrollEndPinnedCount = -1
let scrollEndPinnedTop = -1

// windowSource(): the windowing.rzts host-contract row source — the FILTERED option
// set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
// (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
// wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
// — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
// every framework :key (and every virtual-core measurement key) to `undefined`, which
// recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
// row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
// value, `_opt` the original option (read via wr.row._opt in the windowed template),
// `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
// the same wrapped row the count windows over.
//
// $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
// FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
// would never hit while filtering). The key covers everything the map reads:
// options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
// resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
// Same reference-stability contract the sibling Combobox's filteredOptions carries —
// virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
// unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
const windowSource = $memo(
  () => visibleOptions().map((o, i) => ({ id: valueOf(o), _opt: o, _i: i })),
  () => [$props.options, $data.query, $props.optionValue, $props.optionLabel],
)

// D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
// A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
// pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
// reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
// to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
const pinnedEditIndex = () => -1
const pinnedMeasurement = (pin) => null

// D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
// today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
// windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
// `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
// pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
// in this plan calls these through a type-narrowing wrapper.
//
// GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
// `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
// alongside this were REMOVED here — they were dead code shipped on a mistaken premise
// about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
// virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
// from windowing.rzts; none of those functions' bodies reference the column-axis symbols
// (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
// `colIsOutsideWindow()` do, and Listbox never imports any of those), so
// `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
// 87-16-SUMMARY.md for the verification trail.
const rowsWindowed = () => !!$props.virtual
// autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
// gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
// estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
// column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
// — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
// `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
const autoMeasureOn = () => false

// Keep $data.rows === windowSource() so the windowing math indexes the live option set.
const syncRows = () => { $data.rows = windowSource() }

// SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
// variable-height list at the end while the options in view measure taller than their
// estimate. The view is judged on the DOM, and only at a move the USER made — a move is
// virtual-core's own when it still holds an unreconciled scroll adjustment
// (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
// computed from its last-observed (stale) offset, so it pulls the view back up from the end
// and must neither clear the pin nor be mistaken for the user leaving the end. That position
// is remembered so the scroll event that later reports it is not read as a user move either.
// (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
// with every option measured in the ResizeObserver batch while its offset stays at the stale
// value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
const recordScrollEnd = () => {
  if (!virtualizer || !gridScrollEl || virtualizer.scrollState) return
  const top = gridScrollEl.scrollTop
  if (top === scrollEndPinnedTop) return
  scrollEndPinnedTop = top
  if (virtualizer.scrollAdjustments !== 0) return
  // Only a list that actually overflows has an end to hold: while the window has not painted
  // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
  // recorded then would jump the freshly opened list to the bottom.
  const sh = gridScrollEl.scrollHeight
  const ch = gridScrollEl.clientHeight
  scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1
  scrollEndPinnedCount = windowSource().length
}

// Re-apply the pin after the framework has committed the window (called from the rAF pass):
// the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
// flight, and not when the option count changed since the user reached the end (a new filter
// or appended options must not be auto-followed).
const keepScrollEnd = () => {
  if (!scrollEndPinned || !virtualizer || !gridScrollEl || virtualizer.scrollState) return
  if (windowSource().length !== scrollEndPinnedCount) return
  const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight
  if (maxTop - gridScrollEl.scrollTop > 1) {
    gridScrollEl.scrollTop = maxTop
    scrollEndPinnedTop = gridScrollEl.scrollTop
  }
}

// Defer remeasureWindow() until AFTER the framework commits the recycled window
// (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
// behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
// per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
// SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
// budget), the rAF catches React's async commit. measureElement is idempotent on an
// already-observed node, so running both is cheap and loop-free.
const scheduleRemeasure = () => {
  recordScrollEnd()
  if (remeasurePending) return
  remeasurePending = true
  let ranMicro = false
  const microPass = () => { remeasureWindow() }
  // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
  // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
  // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
  // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
  // committed options cover the virtualizer's window, bounded (the data-table host twin).
  let rafAttempts = 0
  const rafPass = () => {
    const covered = remeasureWindow()
    rafAttempts = rafAttempts + 1
    if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(rafPass)
      return
    }
    keepScrollEnd()
    remeasurePending = false
  }
  if (typeof queueMicrotask !== 'undefined') { ranMicro = true; queueMicrotask(microPass) }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass)
  else if (ranMicro) remeasurePending = false
  else setTimeout(rafPass, 0)
}

// measureElement sweep: hand every rendered windowed option to the virtualizer so its
// true (variable) height is observed (virtual-core measures ONLY nodes passed to
// measureElement, keyed by the data-index attribute). Bails during a programmatic
// scroll (scrollToIndex) so a measure can't starve the scroll target.
const remeasureWindow = () => {
  if (!virtualizer || !gridScrollEl) return true
  if (virtualizer.scrollState) return true
  const els = gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]')
  const rendered = new Set()
  for (const el of els) {
    virtualizer.measureElement(el)
    rendered.add(el.getAttribute('data-index'))
  }
  // N-05: false while the framework has not yet committed the recycled window.
  const items = virtualizer.getVirtualItems()
  for (let i = 0; i < items.length; i++) {
    if (!rendered.has(String(items[i].index))) return false
  }
  return true
}

// ---- focus / scroll helpers (post-mount $refs only) --------------------
// Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
// only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
// verb would override the inherited HTMLElement.focus method on the Lit element.
const focusControl = () => {
  $refs.triggerEl?.focus()
}

// Keep the active option visible inside the scrolling listbox. Reads $refs in
// a post-mount callback only (never eagerly — ROZ123). When windowing, route through
// the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
// scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
const scrollActiveIntoView = () => {
  if ($data.activeIndex < 0) return
  if ($props.virtual && virtualizer) {
    // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
    // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
    // leave it just-unrendered for a frame on the fine-grained targets (Solid).
    virtualizer.scrollToIndex($data.activeIndex, { align: 'center' })
    scheduleRemeasure()
    return
  }
  if (!$refs.listEl) return
  const el = $refs.listEl.querySelector('#' + CSS.escape(optionId($data.activeIndex)))
  el?.scrollIntoView({ block: 'nearest' })
}

// ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
// kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
// re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
// (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
// re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
// asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
// (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
// scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
// (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
// paints (or attempts run out) — idempotent + loop-free.
const kickWindow = (attempts) => {
  if (!virtualizer) return
  gridScrollEl = $el ? $el.querySelector('.rozie-listbox-list') : gridScrollEl
  // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
  // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
  // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
  // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
  if (windowSource().length > 0) {
    syncRows()
    virtualizer.setOptions(virtualizerOptions())
  }
  virtualizer._willUpdate()
  $data.windowVer = $data.windowVer + 1
  remeasureWindow()
  if (windowedRows().length === 0 && attempts > 0) {
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(attempts - 1))
    else setTimeout(() => kickWindow(attempts - 1), 16)
  }
}

// nextAutoId(): a page-wide counter shared by every Rozie component instance. It
// lives on globalThis (read through Reflect, which type-checks in the plain-JS and
// the TS script alike) so separately bundled copies of a leaf never hand out the
// same id. The same four lines live in Combobox, Listbox and Popover.
const nextAutoId = () => {
  const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1
  Reflect.set(globalThis, '__rozieAutoId', n)
  return n
}

// idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
// pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
const idRoot = () => $props.id || $data.autoId || 'rozie-listbox'

$onMount(() => {
  if (!$props.id) $data.autoId = 'rozie-listbox-' + nextAutoId()
  syncRows()
  if ($props.virtual) {
    // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
    // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
    // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
    // Solid/Lit, which leaves the virtualizer with no scroll element.
    gridScrollEl = $el ? $el.querySelector('.rozie-listbox-list') : null
    virtualizer = new Virtualizer(virtualizerOptions())
    virtualizerCleanup = virtualizer._didMount()
    $data.windowVer = $data.windowVer + 1
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(8))
    else setTimeout(() => kickWindow(8), 0)
  }
})

// Reactive re-feed: when the option set (or the query filter) changes, re-sync the
// indexable rows + push fresh options into the virtualizer and re-pull the window
// (the data-table setOptions+_willUpdate re-feed precedent). Watch a derived primitive
// (length + query), never a freshly-built array (the stale-array Pitfall).
$watch(() => (($props.options ? $props.options.length : 0) + '|' + $data.query), () => {
  syncRows()
  if ($props.virtual && virtualizer) {
    gridScrollEl = $el ? $el.querySelector('.rozie-listbox-list') : gridScrollEl
    virtualizer.setOptions(virtualizerOptions())
    virtualizer._willUpdate()
    $data.windowVer = $data.windowVer + 1
    scheduleRemeasure()
  }
})

$onUnmount(() => {
  if (typeTimer !== null) clearTimeout(typeTimer)
  // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
  if (virtualizerCleanup) virtualizerCleanup()
})

// Imperative handle. Shorthand keys (aliased `{ open: fn }` keys are dropped by
// the React emitter) — every function is named exactly as its verb.
$expose({ open, close, toggle, clear, focusControl })
</script>

<listeners>
  <listener :target="document" @click.outside($refs.controlEl,$refs.listEl)="close" r-if="$data.open" />
</listeners>

<template>
<div class="rozie-listbox" :class="{ 'rozie-listbox-open': $data.open, 'rozie-listbox-disabled': $props.disabled, 'rozie-listbox-inline': $props.inline }">

  <!-- Control: the select-only trigger button. It carries role="combobox" per the
       ARIA APG "Select-Only Combobox" pattern (DOM focus stays here; the active
       option is tracked virtually via aria-activedescendant). -->
  <div class="rozie-listbox-control" ref="controlEl">
    <button
      ref="triggerEl"
      type="button"
      class="rozie-listbox-trigger"
      role="combobox"
      aria-haspopup="listbox"
      :aria-expanded="$data.open"
      :aria-controls="idRoot() + '-list'"
      :aria-activedescendant="activeDescendant"
      :aria-label="$props.ariaLabel"
      :disabled="$props.disabled"
      @click="toggle"
      @keydown="onControlKeyDown($event)"
    >
      <slot name="selected" :selected="selectedLabel" :value="$props.value">
        <span r-if="selectedLabel" class="rozie-listbox-selected">{{ selectedLabel }}</span>
        <span r-else class="rozie-listbox-placeholder">{{ $props.placeholder }}</span>
      </slot>
      <span class="rozie-listbox-arrow" aria-hidden="true">▾</span>
    </button>
  </div>

  <!-- Popup listbox (NON-VIRTUAL). aria-activedescendant on the control points at the
       highlighted option id; DOM focus never leaves the control. Byte-identical to the
       pre-windowing render — the `&& !$props.virtual` only routes virtual usage to the
       windowed branch below; with virtual off this is exactly the prior behavior. -->
  <div
    r-if="$data.open && !$props.virtual"
    ref="listEl"
    class="rozie-listbox-list"
    role="listbox"
    :id="idRoot() + '-list'"
    :aria-label="$props.ariaLabel"
    :aria-multiselectable="$props.multiple"
  >
    <div
      r-for="opt, index in visibleOptions()"
      :key="optionId(index)"
      :id="optionId(index)"
      class="rozie-listbox-option"
      :class="{ 'is-active': $data.activeIndex === index, 'is-selected': isSelected(opt), 'is-disabled': disabledOf(opt) }"
      role="option"
      :aria-selected="!!isSelected(opt)"
      :aria-disabled="!!disabledOf(opt)"
      @click="select(opt)"
      @mousemove="onOptionPointerMove(index)"
    >
      <slot name="option" :option="opt" :index="index" :active="$data.activeIndex === index" :selected="isSelected(opt)" :disabled="disabledOf(opt)">
        {{ labelOf(opt) }}
      </slot>
    </div>

    <div r-if="visibleOptions().length === 0" class="rozie-listbox-empty" role="presentation">
      <slot name="empty" :query="$data.query">No options</slot>
    </div>
  </div>

  <!-- ══ WINDOWED listbox (Phase 64 P4, SC-5) — emitted/active ONLY when $props.virtual ══
       Stays MOUNTED whenever virtual (so the .rozie-listbox-list scroll container exists at
       mount for the virtualizer — ROZ123-safe) but is HIDDEN via display:none whenever the
       listbox is closed (CR-01): unmounting would drop the virtualizer's scroll element, so
       we hide-not-unmount to keep its scrollTop + measurements while still honoring
       toggle()/Escape/outside-click close semantics (the non-virtual branch is gated on
       $data.open; this branch mirrors that via the :style display toggle). display:none also
       drops the role="listbox" from the a11y tree when collapsed, so :aria-expanded on the
       control stays consistent. Renders a leading spacer, the windowed { vi, row } slice
       keyed on the wrapper id (CR-02: windowSource() wraps each option into an id-bearing
       row), and a trailing spacer; the option's full-model index is wr.vi.index. The
       container is bounded/scrolling via the base .rozie-listbox-list CSS (max-height from
       the --rozie-listbox-max-height token, mirrored from the maxHeight prop). -->
  <div
    r-if="$props.virtual"
    ref="listEl"
    class="rozie-listbox-list rozie-listbox-list--virtual"
    role="listbox"
    :id="idRoot() + '-list'"
    :aria-label="$props.ariaLabel"
    :aria-multiselectable="$props.multiple"
    :style="($data.open ? '' : 'display:none;') + ($props.maxHeight ? ('height:' + $props.maxHeight + ';max-height:' + $props.maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + $props.maxHeight) : 'overflow-y:auto')"
  >
    <div class="rozie-listbox-spacer" aria-hidden="true" :style="'height:' + padTop() + 'px'"></div>

    <div
      r-for="wr in windowedRows()"
      :key="wr.row.id"
      :id="optionId(wr.vi.index)"
      :data-index="wr.vi.index"
      class="rozie-listbox-option"
      :class="{ 'is-active': $data.activeIndex === wr.vi.index, 'is-selected': isSelected(wr.row._opt), 'is-disabled': disabledOf(wr.row._opt) }"
      role="option"
      :aria-selected="!!isSelected(wr.row._opt)"
      :aria-disabled="!!disabledOf(wr.row._opt)"
      @click="select(wr.row._opt)"
      @mousemove="onOptionPointerMove(wr.vi.index)"
    >
      <slot name="option" :option="wr.row._opt" :index="wr.vi.index" :active="$data.activeIndex === wr.vi.index" :selected="isSelected(wr.row._opt)" :disabled="disabledOf(wr.row._opt)">
        {{ labelOf(wr.row._opt) }}
      </slot>
    </div>

    <div class="rozie-listbox-spacer" aria-hidden="true" :style="'height:' + padBottom() + 'px'"></div>

    <div r-if="windowSource().length === 0" class="rozie-listbox-empty" role="presentation">
      <slot name="empty" :query="$data.query">No options</slot>
    </div>
  </div>
</div>
</template>

<style>
/*
  Fully token-driven. EVERY value is a `var(--rozie-listbox-*, <fallback>)`, so
  the component renders with zero configuration yet is completely re-skinnable
  by setting tokens at any ancestor scope (`:root`, `.dark`, a wrapper, or the
  `.rozie-listbox` element itself). The shipped `themes/*.css` presets do exactly
  that — mapping these tokens onto shadcn/Radix, Material 3, and Bootstrap 5.
  Nested var() fallbacks let one token derive from another (ring ← accent,
  popup-bg ← bg) while staying independently overridable.
*/
.rozie-listbox {
  position: relative;
  display: inline-block;
  min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
  font: var(--rozie-listbox-font, inherit);
}
.rozie-listbox-control { display: block; }

.rozie-listbox-input,
.rozie-listbox-trigger {
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
  padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
  font: inherit;
  text-align: left;
  background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
  border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
  cursor: pointer;
}
.rozie-listbox-input { cursor: text; }
.rozie-listbox-input:focus-visible,
.rozie-listbox-input:focus,
.rozie-listbox-trigger:focus-visible,
.rozie-listbox-trigger:focus {
  outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
  outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
}
.rozie-listbox-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
.rozie-listbox-placeholder { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
.rozie-listbox-arrow {
  font-size: 0.75em;
  color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
  opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
}

.rozie-listbox-list {
  position: absolute;
  z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
  top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
  max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
  overflow-y: auto;
  list-style: none;
  background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
  border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
  box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
}

/* Inline mode: the root fills its container (so the control + list span the full
   width even when a host wrapper — lit custom element / angular host — sits
   between it and a stretching flex parent), and the list renders IN FLOW so an
   overflow-clipped ancestor (e.g. a command-palette panel) can't cull it. */
.rozie-listbox-inline {
  display: block;
  width: 100%;
}
.rozie-listbox-inline .rozie-listbox-list {
  position: static;
  margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.rozie-listbox-option {
  padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
  border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
  color: var(--rozie-listbox-option-fg, inherit);
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
}
.rozie-listbox-option.is-active {
  background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
  color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
}
.rozie-listbox-option.is-selected {
  background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
  color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
  font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
}
.rozie-listbox-option.is-selected::after {
  content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
  color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
}
.rozie-listbox-option.is-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
.rozie-listbox-empty { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
/* Windowing spacer rows (Phase 64 P4): zero-chrome blocks whose inline height keeps the
   total scroll height === virtual-core getTotalSize() (the windowed slice sits between). */
.rozie-listbox-spacer { margin: 0; padding: 0; border: 0; flex: none; }
/* The windowing engine compensates for options above the viewport changing height itself.
   Browser scroll anchoring moved the view again when the leading spacer changed height, and
   that move was indistinguishable from the user scrolling away from the end (measured: the
   scroll-end pin was cleared at a 38px-short position). */
.rozie-listbox-list--virtual { overflow-anchor: none; }
</style>

</rozie>

…and Rozie compiles it to six framework-native components. Switch the tabs to see the actual generated output for each target (this is exactly what ships in @rozie-ui/listbox-{react,vue,svelte,angular,solid,lit}):

tsx
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { clsx, parseInlineStyle, rozieAttr, rozieDisplay, useControllableState, useOutsideClick } from '@rozie/runtime-react';
import './Listbox.css';
// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';

// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.

interface SelectedCtx { selected: any; value: any; }

interface OptionCtx { option: any; index: any; active: any; selected: any; disabled: any; }

interface EmptyCtx { query: any; }

interface ListboxProps extends Omit<import('react').ComponentPropsWithoutRef<'div'>, 'options' | 'value' | 'defaultValue' | 'onValueChange' | 'multiple' | 'inline' | 'disabled' | 'placeholder' | 'closeOnSelect' | 'optionLabel' | 'optionValue' | 'optionDisabled' | 'id' | 'ariaLabel' | 'virtual' | 'estimateRowHeight' | 'maxHeight' | 'onOpenChange' | 'onChange' | 'renderSelected' | 'renderOption' | 'renderEmpty' | 'slots' | 'children' | 'dangerouslySetInnerHTML'> {
  /**
   * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
   */
  options?: any[];
  /**
   * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
   * @example
   * <Listbox value={fruit} onValueChange={setFruit} options={fruits} />
   */
  value?: (unknown) | null;
  defaultValue?: (unknown) | null;
  onValueChange?: (value: (unknown) | null) => void;
  /**
   * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
   */
  multiple?: boolean;
  /**
   * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
   */
  inline?: boolean;
  /**
   * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
   */
  disabled?: boolean;
  /**
   * Placeholder text shown in the empty control.
   */
  placeholder?: string;
  /**
   * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
   */
  closeOnSelect?: boolean;
  /**
   * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
   */
  optionLabel?: ((...args: any[]) => any) | null;
  /**
   * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
   */
  optionValue?: ((...args: any[]) => any) | null;
  /**
   * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
   */
  optionDisabled?: ((...args: any[]) => any) | null;
  /**
   * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
   */
  id?: string;
  /**
   * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
   */
  ariaLabel?: (string) | null;
  /**
   * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
   */
  virtual?: boolean;
  /**
   * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
   */
  estimateRowHeight?: number;
  /**
   * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
   */
  maxHeight?: string;
  onOpenChange?: (...args: any[]) => void;
  onChange?: (...args: any[]) => void;
  renderSelected?: (ctx: SelectedCtx) => ReactNode;
  renderOption?: (ctx: OptionCtx) => ReactNode;
  renderEmpty?: (ctx: EmptyCtx) => ReactNode;
  slots?: Record<string, () => import('react').ReactNode>;
}

export interface ListboxHandle {
  open: (...args: any[]) => any;
  close: (...args: any[]) => any;
  toggle: (...args: any[]) => any;
  clear: (...args: any[]) => any;
  focusControl: (...args: any[]) => any;
}

const Listbox = forwardRef<ListboxHandle, ListboxProps>(function Listbox(_props: ListboxProps, ref): JSX.Element {
  const __defaultOptions = useState(() => (() => [])())[0];
  const props: Omit<ListboxProps, 'options' | 'multiple' | 'inline' | 'disabled' | 'placeholder' | 'closeOnSelect' | 'optionLabel' | 'optionValue' | 'optionDisabled' | 'id' | 'ariaLabel' | 'virtual' | 'estimateRowHeight' | 'maxHeight'> & { options: any[]; multiple: boolean; inline: boolean; disabled: boolean; placeholder: string; closeOnSelect: boolean; optionLabel: ((...args: any[]) => any) | null; optionValue: ((...args: any[]) => any) | null; optionDisabled: ((...args: any[]) => any) | null; id: string; ariaLabel: (string) | null; virtual: boolean; estimateRowHeight: number; maxHeight: string } = {
    ..._props,
    options: _props.options ?? __defaultOptions,
    multiple: _props.multiple ?? false,
    inline: _props.inline ?? false,
    disabled: _props.disabled ?? false,
    placeholder: _props.placeholder ?? '',
    closeOnSelect: _props.closeOnSelect ?? true,
    optionLabel: _props.optionLabel ?? null,
    optionValue: _props.optionValue ?? null,
    optionDisabled: _props.optionDisabled ?? null,
    id: _props.id ?? '',
    ariaLabel: _props.ariaLabel ?? null,
    virtual: _props.virtual ?? false,
    estimateRowHeight: _props.estimateRowHeight ?? 36,
    maxHeight: _props.maxHeight ?? '',
  };
  const attrs: Record<string, unknown> = (() => {
    const { options, value, multiple, inline, disabled, placeholder, closeOnSelect, optionLabel, optionValue, optionDisabled, id, ariaLabel, virtual, estimateRowHeight, maxHeight, defaultValue, onValueChange, onOpenChange, onChange, ...rest } = _props as ListboxProps & Record<string, unknown>;
    void options; void value; void multiple; void inline; void disabled; void placeholder; void closeOnSelect; void optionLabel; void optionValue; void optionDisabled; void id; void ariaLabel; void virtual; void estimateRowHeight; void maxHeight; void defaultValue; void onValueChange; void onOpenChange; void onChange;
    return rest;
  })();
  const gridScrollEl = useRef<any>(null);
  const virtualizer = useRef<any>(null);
  const virtualizerCleanup = useRef<any>(null);
  const measuredRowCount = useRef(0);
  const measuredRowTotal = useRef(0);
  const windowVerBumpPending = useRef(false);
  const remeasurePending = useRef(false);
  const scrollEndPinnedTop = useRef(-1);
  const scrollEndPinned = useRef(false);
  const scrollEndPinnedCount = useRef(-1);
  const typeTimer = useRef<any>(null);
  const typeBuffer = useRef('');
  const [value, setValue] = useControllableState({
    value: props.value,
    defaultValue: props.defaultValue ?? null,
    onValueChange: props.onValueChange,
  });
  const _idRef = useRef(props.id);
  _idRef.current = props.id;
  const _virtualRef = useRef(props.virtual);
  _virtualRef.current = props.virtual;
  const [autoId, setAutoId] = useState('');
  const [open$local, setOpen$local] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [query, setQuery] = useState('');
  const [rows, setRows] = useState<any[]>([]);
  const [windowVer, setWindowVer] = useState(0);
  const [editVer, setEditVer] = useState(0);
  const controlEl = useRef<HTMLDivElement | null>(null);
  const triggerEl = useRef<HTMLButtonElement | null>(null);
  const listEl = useRef<HTMLDivElement | null>(null);
  const __rozieRoot = useRef<HTMLDivElement | null>(null);
  const selectedLabel = useMemo(() => {
    const cur = value;
    if (props.multiple) {
      // Read the model value into a local before narrowing: `$props.value` lowers
      // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
      // separate calls — narrowing one stable local works on every target.
      const arr = Array.isArray(cur) ? cur : [];
      if (arr.length === 0) return '';
      return props.options.filter((o: any) => arr.includes(valueOf(o))).map(labelOf).join(', ');
    }
    const match = props.options.find((o: any) => valueOf(o) === cur);
    return match === undefined ? '' : labelOf(match);
  }, [Array, labelOf, props.multiple, props.options, value, valueOf]);
  const activeDescendant = useMemo(() => {
    if (!open$local || activeIndex < 0) return null;
    return optionId(activeIndex);
  }, [activeIndex, open$local, optionId]);
  const _watch0First = useRef(true);

  // ---- shared list spine (P2: @rozie-ui/headless-core/listCore.rzts) ------
  // The option resolvers, client filter, enabled-index navigation, the keyboard
  // reducer, type-ahead, single+multi selection, open/close state, and
  // activeDescendant derivation now live in the shared, focus-/input-mode
  // parameterized list spine. It is a compile-time `.rzts` script-partial: it
  // dissolves into this leaf via inlineScriptPartials() before IR lowering (zero
  // runtime dep). Listbox consumes it in focus-model `activedescendant` +
  // input-mode `select-only` + multi + type-ahead. The spine closes over this
  // host's pieces by convention: the reassigned module-`let`s typeBuffer/typeTimer
  // (above) and the impure ref fns focusControl/scrollActiveIntoView (below).
  // Type-ahead buffer for the select-only listbox trigger. Module-scope
  // `let`s reassigned from handlers → the React emitter hoists them to `useRef`
  // so they persist across renders (the setup-once guarantee); no-op elsewhere.
  // They STAY in this host (not the shared spine) per the A==B rule: reassigned
  // module-`let`s + sigils live in the host; the partial only closes over them.

  // ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
  // Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
  // partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
  // navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
  // selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
  // script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
  // IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
  //
  // ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
  // The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
  // windowing.rzts uses) along two axes:
  //   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
  //     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
  //     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
  //     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
  //     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
  //     activedescendant model.
  //   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
  //     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
  //     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
  //     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
  //     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
  //     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
  //
  // ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
  //   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
  //     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
  //     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
  //     over them (in `onTypeahead`).
  //   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
  //     generates in $onMount); `optionId` below derives every option id from it.
  //   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
  //     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
  //     HOST functions; this partial only closes over them (it reads NO refs itself).
  //   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
  //     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
  //     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
  //     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
  //     `$data.query`), NOT a discriminant prop.

  // ---- option resolvers --------------------------------------------------
  function labelOf(opt: any) {
    if (props.optionLabel !== null) return props.optionLabel(opt);
    if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
    return String(opt);
  }
  function valueOf(opt: any) {
    if (props.optionValue !== null) return props.optionValue(opt);
    if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
    return opt;
  }
  function disabledOf(opt: any) {
    if (props.optionDisabled !== null) return !!props.optionDisabled(opt);
    if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
    return false;
  }
  // `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
  // per-instance id) so the option ids follow the host's auto-id fallback.
  function optionId(index: any) {
    return idRoot() + '-opt-' + index;
  }

  // ---- derived state -----------------------------------------------------
  // The visible option list: identity in select-only / non-filtering mode,
  // a case-insensitive substring filter when a combobox query is present.
  // A plain function (not `$computed`) so it reads uniformly across all six
  // targets — a `$computed` is a value on React but an accessor on Solid, so
  // aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
  // plain function is identical everywhere.
  function visibleOptions() {
    const q = (query || '').trim().toLowerCase();
    if (q === '') return props.options;
    return props.options.filter((opt: any) => labelOf(opt).toLowerCase().includes(q));
  }

  // The label shown in the (select-only) trigger when closed. A real `$computed`
  // — read bare in the template, never aliased in script, so the per-target
  // accessor form stays uniform.
  // Is a given option currently selected? Multi compares array membership.
  function isSelected(opt: any) {
    const v = valueOf(opt);
    const cur = value;
    if (props.multiple) return Array.isArray(cur) && cur.includes(v);
    return cur === v;
  }

  // First enabled visible index, preferring the currently-selected option.
  function resolveInitialActive() {
    const opts = visibleOptions();
    const sel = opts.findIndex((o: any) => isSelected(o) && !disabledOf(o));
    if (sel !== -1) return sel;
    return opts.findIndex((o: any) => !disabledOf(o));
  }

  // ---- open / close ------------------------------------------------------
  // Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
  // $emit's directly — no longer funneled through a single wrapper. The
  // former "route every emit through ONE wrapper fn" workaround guarded
  // against a React duplicate `const {onOpenChange}=props` per emit-site
  // (TS2451); verified against the current emitter (target-react
  // `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
  // hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
  // target into exactly one destructure, and a non-escaping function (e.g.
  // `open`, reachable here only via `$expose`) never destructures at all — so
  // no combination of these three functions can produce the duplicate-const
  // shape. See project_next_port_listbox / project_emitter_hardening_backlog.
  function open() {
    if (props.disabled) return;
    if (open$local) return;
    setOpen$local(true);
    setActiveIndex(resolveInitialActive());
    props.onOpenChange && props.onOpenChange({
      open: true
    });
  }
  const { onOpenChange: _rozieProp_onOpenChange } = props;
    const close = useCallback(() => {
    if (!open$local) return;
    setOpen$local(false);
    setActiveIndex(-1);
    _rozieProp_onOpenChange && _rozieProp_onOpenChange({
      open: false
    });
  }, [_rozieProp_onOpenChange, open$local]);
  const toggle = useCallback(() => {
    if (open$local) close();else open();
  }, [close, open, open$local]);
  // ---- selection ---------------------------------------------------------
  const { onChange: _rozieProp_onChange } = props;
    const select = useCallback((opt: any) => {
    if (disabledOf(opt)) return;
    const v = valueOf(opt);
    if (props.multiple) {
      const cur = value;
      const arr = Array.isArray(cur) ? cur : [];
      // Fresh array on every commit — in-place mutation is dropped by the
      // React/Solid/Lit/Angular change detectors.
      const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
      setValue(next);
      _rozieProp_onChange && _rozieProp_onChange({
        value: next,
        option: opt
      });
    } else {
      setValue(v);
      _rozieProp_onChange && _rozieProp_onChange({
        value: v,
        option: opt
      });
      if (props.closeOnSelect) {
        close();
        focusControl();
      }
    }
  }, [_rozieProp_onChange, close, disabledOf, focusControl, props.closeOnSelect, props.multiple, setValue, value, valueOf]);
  function clear() {
    const empty = props.multiple ? [] : null;
    setValue(empty);
    setQuery('');
    props.onChange && props.onChange({
      value: empty,
      option: null
    });
  }

  // ---- keyboard navigation over the VISIBLE list -------------------------
  function nextEnabled(from: any, dir: any) {
    const opts = visibleOptions();
    if (opts.length === 0) return -1;
    let i = from;
    for (let step = 0; step < opts.length; step++) {
      i += dir;
      if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
      if (!disabledOf(opts[i])) return i;
    }
    return from;
  }
  function move(dir: any) {
    if (!open$local) {
      open();
      return;
    }
    const start = activeIndex < 0 ? dir > 0 ? -1 : 0 : activeIndex;
    setActiveIndex(nextEnabled(start, dir));
    scrollActiveIntoView();
  }
  function moveEdge(toEnd: any) {
    if (!open$local) open();
    setActiveIndex(toEnd ? nextEnabled(-1, -1) : nextEnabled(-1, 1));
    scrollActiveIntoView();
  }
  function commitActive() {
    const opts = visibleOptions();
    if (activeIndex >= 0 && activeIndex < opts.length) select(opts[activeIndex]);
  }

  // Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
  // first option whose label starts with the buffer.
  function onTypeahead(ch: any) {
    if (typeTimer.current !== null) clearTimeout(typeTimer.current);
    typeBuffer.current += ch.toLowerCase();
    typeTimer.current = setTimeout(() => {
      typeBuffer.current = '';
    }, 600);
    const opts = visibleOptions();
    const idx = opts.findIndex((o: any) => !disabledOf(o) && labelOf(o).toLowerCase().startsWith(typeBuffer.current));
    if (idx !== -1) {
      if (!open$local) open();
      setActiveIndex(idx);
      scrollActiveIntoView();
    }
  }

  // Key handler shared by the trigger and the combobox input. The printable-
  // character branch is reached only in select-only mode (the combobox input
  // types through @input).
  const onControlKeyDown = useCallback(($event: any) => {
    const key = $event.key;
    if (key === 'ArrowDown') {
      $event.preventDefault();
      move(1);
    } else if (key === 'ArrowUp') {
      $event.preventDefault();
      move(-1);
    } else if (key === 'Home') {
      $event.preventDefault();
      moveEdge(false);
    } else if (key === 'End') {
      $event.preventDefault();
      moveEdge(true);
    } else if (key === 'Enter') {
      if (open$local) {
        $event.preventDefault();
        commitActive();
      }
    } else if (key === 'Escape') {
      if (open$local) {
        $event.preventDefault();
        close();
        focusControl();
      }
    } else if (key === ' ' || key === 'Spacebar') {
      // Space toggles / commits in a select-only host (a button trigger). A
      // filter-input host types the literal space into its <input> and does NOT
      // route Space through this reducer, so this branch is select-only by use.
      $event.preventDefault();
      if (!open$local) open();else commitActive();
    } else if (key === 'Tab') {
      if (open$local) close();
    } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
      onTypeahead(key);
    }
  }, [close, commitActive, focusControl, move, moveEdge, onTypeahead, open, open$local]);
  // Combobox input handler: keep the popup open while typing, reset the active
  // highlight to the first match, and surface the query for remote filtering.
  // Pointer hover sets the virtual highlight (matches native <select> feel).
  const onOptionPointerMove = useCallback((index: any) => {
    if (activeIndex !== index) setActiveIndex(index);
  }, [activeIndex]);
  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
  // in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
  // compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
  //
  // HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
  // by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
  //   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
  //                             returns its pre-pagination row model, listbox/combobox return the
  //                             filtered options). This partial MUST NOT reach into the host data engine
  //                             directly — rows arrive ONLY through windowSource().
  //   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
  //   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
  //   - gridScrollEl              — the scroll-container element handle.
  //   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
  //   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
  //   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
  //   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
  //                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
  //                             listbox passes nothing. Routing pinning through this host hook (NOT
  //                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
  //   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
  //                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
  //                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
  //                             convention exactly as they already call `pinnedEditIndex()`.
  //   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
  //                             host until it defines the real column-axis mechanism (87-04+).
  //   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
  //                             no default.
  //   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
  //                             from table-core's `getSize()` under D-06. REQUIRED, no default.
  //   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
  //                             DataTable host unions pinned + active-cell + editing column indices into
  //                             the column-window slice; listbox/combobox pass an empty array (host-
  //                             provided, defaulting to `[]`).
  //   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
  //                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
  //   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
  //                             data-table's real body reads `$props.autoMeasure === true`; listbox/
  //                             combobox/command-palette return `false` so the accumulator branch
  //                             estimateRowSize() gates on is dead code for them (D-20).
  //   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
  //                             assigned to refineRowEstimate() (below) by the host. The DataTable
  //                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
  //                             measureElement sweep so the fold + hysteresis re-feed run on every
  //                             window commit. Routed through a mutable `let` rather than a direct
  //                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
  //                             calling a bare-specifier-partial CONST is the exact forward-reference
  //                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
  //                             documents for columnVirtualizerOptions() (inlineScriptPartials()
  //                             groups the relative partial BEFORE the bare-specifier partial in the
  //                             merged per-target output, regardless of source import order). A
  //                             mutable `let` hoists to `useRef` on React and is excluded from a
  //                             useCallback's dependency array, sidestepping the hazard entirely — the
  //                             SAME mechanism `refreshRowModel` already relies on.
  //
  // AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
  // LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
  // note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
  // (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
  // `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
  // `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
  // (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
  // (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
  // (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
  // (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
  // axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
  // sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
  // Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
  // (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
  // hardcoded constant, separate from the row axis's `overscan: 8` below).
  //
  // isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
  // immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
  // signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
  // mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.

  // getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
  // React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
  // id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
  function virtualItemKey(i: any) {
    const src = windowSource();
    return src && src[i] ? src[i].id : undefined;
  }

  // COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
  // row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
  // cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
  // overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
  // horizontal Virtualizer instance (see the AXIS MECHANISM note above).

  // ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
  // mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
  // hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
  // `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
  // precedent), so two DataTable instances on one page never share an accumulator
  // (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
  // row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
  // virtual-core (the hysteresis comparison baseline). ══

  // ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
  // write behind a SINGLE microtask-deferred increment, regardless of how many callers request
  // one within the same synchronous JS task. ══
  //
  // ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
  // confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
  // (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
  // `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
  // TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
  // framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
  // (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
  // ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
  // synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
  // measured rows from the flat seed to their real heights) can fire onChange, and therefore an
  // UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
  // refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
  // folded those same measurements into the running mean or re-fed the converged estimate into
  // virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
  // getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
  // resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
  // reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
  // effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
  // resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
  // refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
  // reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
  // synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
  // correct by construction. Solid does not auto-batch a signal write made from outside a
  // Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
  // externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
  // virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
  // getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
  // the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
  // recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
  // further row happens to differ from its cache — so the DOM stays frozen on that stale,
  // pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
  // converged correctly (T-87-07's own confirmed finding).
  //
  // FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
  // refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
  // idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
  // EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
  // fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
  // batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
  // column widths come from table-core's getSize() oracle, never measured from the DOM), so
  // columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
  function bumpWindowVer(): void {
    if (windowVerBumpPending.current) return;
    windowVerBumpPending.current = true;
    const flush = () => {
      windowVerBumpPending.current = false;
      setWindowVer(prev => prev + 1);
    };
    // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
    // (virtualization.rzts) for environments where queueMicrotask is unavailable.
    if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
  }

  // ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
  // virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
  // re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
  // own measureElement/resizeItem idempotence (see refineRowEstimate() below).
  // estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
  // FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
  // verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
  // regardless of autoMeasure) still returns the seed — the very first render has nothing
  // measured yet either way (D-15).
  function estimateRowSize(i: number): number {
    if (!autoMeasureOn()) return props.estimateRowHeight;
    if (measuredRowCount.current === 0) return props.estimateRowHeight;
    return Math.round(measuredRowTotal.current / measuredRowCount.current);
  }

  // foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
  // accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
  // `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
  // source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
  // Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
  // on virtual-core's generic option inference. onChange's windowVer write is routed through
  // bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
  // this onChange MANY times in a single synchronous sweep (once per row whose real measured
  // size differs from its cache, e.g. every never-before-measured row in the FIRST window),
  // and coalescing those into one microtask-deferred write is what keeps every target's render
  // landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
  // Solid-specific rendering gap this closes). The React emitter still lowers the underlying
  // `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
  // microtask, exactly as it was correct from a mount closure before.
  const virtualizerOptions = useCallback((): any => ({
    count: windowSource().length,
    getScrollElement: () => gridScrollEl.current,
    estimateSize: (i: any) => estimateRowSize(i),
    observeElementRect,
    observeElementOffset,
    scrollToFn: elementScroll,
    measureElement,
    overscan: 8,
    getItemKey: virtualItemKey,
    onChange: () => {
      bumpWindowVer();
      // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
      // virtual-core only observe()s a node you explicitly hand to measureElement (it does
      // NOT auto-discover rendered rows — measureElement is the SOLE caller of
      // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
      // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
      // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
      // so the new <tr> set is in the DOM before we measure. Safe from an infinite
      // measure→onChange→measure loop: measureElement is idempotent on an already-observed
      // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
      // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
      // re-measure is a no-op.
      scheduleRemeasure();
    }
  }), [bumpWindowVer, estimateRowSize, scheduleRemeasure, virtualItemKey, windowSource]);
  // pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
  // shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
  // two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
  // no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
  // `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
  // guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
  // thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
  // gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
  // Typing-only: the runtime value (a measurement or null) is unchanged.
  function pinMeasurement(pin: number): {
    start: number;
    size: number;
    index: number;
    end: number;
  } | null {
    return pinnedMeasurement(pin);
  }

  // windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
  // { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
  // $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
  // full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
  // `rows` binding → TS2448 self-shadow, line ~1149 lesson).
  function windowedRows() {
    // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
    // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
    // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
    // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
    // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
    // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
    // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
    // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
    // first re-run that picks up the now-non-null virtualizer.
    // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
    // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
    void windowVer;
    void editVer;
    if (!virtualizer.current) {
      // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
      // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
      // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
      // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
      // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
      // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
      // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
      // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
      // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
      // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
      // the first onChange after _didMount.
      if (!rowsWindowed()) {
        const rowList = rows || [];
        return rowList.map((r: any, i: any) => ({
          vi: {
            index: i
          },
          row: r
        }));
      }
      return [];
    }
    const items = virtualizer.current.getVirtualItems();
    const rowList = rows || [];
    // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
    // shrink window where the virtualizer count is stale relative to $data.rows on the async
    // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
    // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
    const out = items.map((vi: any) => ({
      vi,
      row: rowList[vi.index]
    })).filter((wr: any) => wr.row);
    // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
    // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
    // into another full-model row), LEADING the slice when it sits above the window and TRAILING
    // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
    // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
    // into the real windowing.
    const pin = pinnedEditIndex();
    if (pin >= 0 && rowList[pin]) {
      let inWindow = false;
      for (let i = 0; i < items.length; i++) {
        if (items[i].index === pin) {
          inWindow = true;
          break;
        }
      }
      if (!inWindow) {
        const pm = pinMeasurement(pin);
        const firstStart = items.length ? items[0].start : 0;
        const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
        const pinnedEntry = {
          vi: pm != null ? pm : {
            index: pin
          },
          row: rowList[pin],
          pinned: true
        };
        if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
      }
    }
    return out;
  }

  // Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
  // the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
  // (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
  function padTop() {
    // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
    // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
    // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
    void windowVer;
    void editVer;
    if (!rowsWindowed() || !virtualizer.current) return 0;
    const items = virtualizer.current.getVirtualItems();
    let pad = items.length ? items[0].start : 0;
    // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
    // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
    // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
    const pin = pinnedEditIndex();
    if (pin >= 0) {
      const pm = pinMeasurement(pin);
      const inWindow = pmIndexInWindow(items, pin);
      if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
    }
    return pad < 0 ? 0 : pad;
  }
  function padBottom() {
    // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
    // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
    // on pin/unpin.
    void windowVer;
    void editVer;
    if (!rowsWindowed() || !virtualizer.current) return 0;
    const items = virtualizer.current.getVirtualItems();
    if (!items.length) return 0;
    let pad = virtualizer.current.getTotalSize() - items[items.length - 1].end;
    // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
    // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
    const pin = pinnedEditIndex();
    if (pin >= 0) {
      const pm = pinMeasurement(pin);
      const inWindow = pmIndexInWindow(items, pin);
      // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
      // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
      // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
      // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
      // the offset comparison only if the measurement lacks an index (defensive).
      const lastItemIdx = items[items.length - 1].index;
      const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
      if (pm && !inWindow && below) {
        // below the window → it trailed the slice; subtract its height from the trailing spacer.
        if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
      }
    }
    return pad < 0 ? 0 : pad;
  }
  // pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
  function pmIndexInWindow(items: any, idx: any) {
    for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
    return false;
  }
  // rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
  // window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
  function rowIsOutsideWindow(r: any) {
    if (!rowsWindowed() || !virtualizer.current) return false;
    const items = virtualizer.current.getVirtualItems();
    for (const it of items as any) if (it.index === r) return false;
    return true;
  }

  // ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
  // rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
  // a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
  // returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
  // cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══

  // windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
  // Windowing instance state (the `let table` precedent — React hoists reassigned
  // module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
  // when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
  // virtualizer observes; remeasurePending dedupes the deferred sweep.

  // Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
  // the option count at that moment, and the last scrollTop already accounted for.
  // windowSource(): the windowing.rzts host-contract row source — the FILTERED option
  // set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
  // (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
  // wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
  // — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
  // every framework :key (and every virtual-core measurement key) to `undefined`, which
  // recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
  // row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
  // value, `_opt` the original option (read via wr.row._opt in the windowed template),
  // `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
  // the same wrapped row the count windows over.
  //
  // $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
  // FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
  // would never hit while filtering). The key covers everything the map reads:
  // options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
  // resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
  // Same reference-stability contract the sibling Combobox's filteredOptions carries —
  // virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
  // unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
  const windowSourceCache = useMemo(() => ({
    keys: null as any[] | null,
    val: null as any
  }), []);
  function windowSource() {
    const __rozieMemoKey = [props.options, query, props.optionValue, props.optionLabel];
    const __rozieMemoPrev = windowSourceCache.keys;
    if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
      return windowSourceCache.val;
    }
    const __rozieMemoVal = visibleOptions().map((o: any, i: any) => ({
      id: valueOf(o),
      _opt: o,
      _i: i
    }));
    windowSourceCache.keys = __rozieMemoKey;
    windowSourceCache.val = __rozieMemoVal;
    return __rozieMemoVal;
  }
  // D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
  // A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
  // pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
  // reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
  // to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
  function pinnedEditIndex() {
    return -1;
  }
  function pinnedMeasurement(pin: any) {
    return null;
  }

  // D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
  // today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
  // windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
  // `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
  // pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
  // in this plan calls these through a type-narrowing wrapper.
  //
  // GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
  // `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
  // alongside this were REMOVED here — they were dead code shipped on a mistaken premise
  // about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
  // virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
  // from windowing.rzts; none of those functions' bodies reference the column-axis symbols
  // (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
  // `colIsOutsideWindow()` do, and Listbox never imports any of those), so
  // `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
  // 87-16-SUMMARY.md for the verification trail.
  function rowsWindowed() {
    return !!props.virtual;
  }
  // autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
  // gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
  // estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
  // column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
  // — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
  // `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
  function autoMeasureOn() {
    return false;
  }

  // Keep $data.rows === windowSource() so the windowing math indexes the live option set.
  const syncRows = useCallback(() => {
    setRows(windowSource());
  }, [windowSource]);
  // SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
  // variable-height list at the end while the options in view measure taller than their
  // estimate. The view is judged on the DOM, and only at a move the USER made — a move is
  // virtual-core's own when it still holds an unreconciled scroll adjustment
  // (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
  // computed from its last-observed (stale) offset, so it pulls the view back up from the end
  // and must neither clear the pin nor be mistaken for the user leaving the end. That position
  // is remembered so the scroll event that later reports it is not read as a user move either.
  // (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
  // with every option measured in the ResizeObserver batch while its offset stays at the stale
  // value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
  function recordScrollEnd() {
    if (!virtualizer.current || !gridScrollEl.current || virtualizer.current.scrollState) return;
    const top = gridScrollEl.current.scrollTop;
    if (top === scrollEndPinnedTop.current) return;
    scrollEndPinnedTop.current = top;
    if (virtualizer.current.scrollAdjustments !== 0) return;
    // Only a list that actually overflows has an end to hold: while the window has not painted
    // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
    // recorded then would jump the freshly opened list to the bottom.
    const sh = gridScrollEl.current.scrollHeight;
    const ch = gridScrollEl.current.clientHeight;
    scrollEndPinned.current = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
    scrollEndPinnedCount.current = windowSource().length;
  }

  // Re-apply the pin after the framework has committed the window (called from the rAF pass):
  // the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
  // flight, and not when the option count changed since the user reached the end (a new filter
  // or appended options must not be auto-followed).
  function keepScrollEnd() {
    if (!scrollEndPinned.current || !virtualizer.current || !gridScrollEl.current || virtualizer.current.scrollState) return;
    if (windowSource().length !== scrollEndPinnedCount.current) return;
    const maxTop = gridScrollEl.current.scrollHeight - gridScrollEl.current.clientHeight;
    if (maxTop - gridScrollEl.current.scrollTop > 1) {
      gridScrollEl.current.scrollTop = maxTop;
      scrollEndPinnedTop.current = gridScrollEl.current.scrollTop;
    }
  }

  // Defer remeasureWindow() until AFTER the framework commits the recycled window
  // (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
  // behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
  // per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
  // SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
  // budget), the rAF catches React's async commit. measureElement is idempotent on an
  // already-observed node, so running both is cheap and loop-free.
  function scheduleRemeasure() {
    recordScrollEnd();
    if (remeasurePending.current) return;
    remeasurePending.current = true;
    let ranMicro = false;
    const microPass = () => {
      remeasureWindow();
    };
    // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
    // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
    // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
    // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
    // committed options cover the virtualizer's window, bounded (the data-table host twin).
    let rafAttempts = 0;
    const rafPass = () => {
      const covered = remeasureWindow();
      rafAttempts = rafAttempts + 1;
      if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
        requestAnimationFrame(rafPass);
        return;
      }
      keepScrollEnd();
      remeasurePending.current = false;
    };
    if (typeof queueMicrotask !== 'undefined') {
      ranMicro = true;
      queueMicrotask(microPass);
    }
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) remeasurePending.current = false;else setTimeout(rafPass, 0);
  }

  // measureElement sweep: hand every rendered windowed option to the virtualizer so its
  // true (variable) height is observed (virtual-core measures ONLY nodes passed to
  // measureElement, keyed by the data-index attribute). Bails during a programmatic
  // scroll (scrollToIndex) so a measure can't starve the scroll target.
  function remeasureWindow() {
    if (!virtualizer.current || !gridScrollEl.current) return true;
    if (virtualizer.current.scrollState) return true;
    const els = gridScrollEl.current.querySelectorAll('.rozie-listbox-option[data-index]');
    const rendered = new Set();
    for (const el of els as any) {
      virtualizer.current.measureElement(el);
      rendered.add(el.getAttribute('data-index'));
    }
    // N-05: false while the framework has not yet committed the recycled window.
    const items = virtualizer.current.getVirtualItems();
    for (let i = 0; i < items.length; i++) {
      if (!rendered.has(String(items[i].index))) return false;
    }
    return true;
  }

  // ---- focus / scroll helpers (post-mount $refs only) --------------------
  // Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
  // only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
  // verb would override the inherited HTMLElement.focus method on the Lit element.
  function focusControl() {
    triggerEl.current?.focus();
  }

  // Keep the active option visible inside the scrolling listbox. Reads $refs in
  // a post-mount callback only (never eagerly — ROZ123). When windowing, route through
  // the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
  // scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
  function scrollActiveIntoView() {
    if (activeIndex < 0) return;
    if (props.virtual && virtualizer.current) {
      // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
      // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
      // leave it just-unrendered for a frame on the fine-grained targets (Solid).
      virtualizer.current.scrollToIndex(activeIndex, {
        align: 'center'
      });
      scheduleRemeasure();
      return;
    }
    if (!listEl.current) return;
    const el = listEl.current!.querySelector('#' + CSS.escape(optionId(activeIndex)));
    el?.scrollIntoView({
      block: 'nearest'
    });
  }

  // ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
  // kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
  // re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
  // (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
  // re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
  // asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
  // (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
  // scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
  // (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
  // paints (or attempts run out) — idempotent + loop-free.
  const kickWindow = useCallback((attempts: any) => {
    if (!virtualizer.current) return;
    gridScrollEl.current = __rozieRoot.current ? __rozieRoot.current!.querySelector('.rozie-listbox-list') : gridScrollEl.current;
    // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
    // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
    // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
    // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
    if (windowSource().length > 0) {
      syncRows();
      virtualizer.current.setOptions(virtualizerOptions());
    }
    virtualizer.current._willUpdate();
    setWindowVer(prev => prev + 1);
    remeasureWindow();
    if (windowedRows().length === 0 && attempts > 0) {
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(attempts - 1));else setTimeout(() => kickWindow(attempts - 1), 16);
    }
  }, [remeasureWindow, syncRows, virtualizerOptions, windowSource, windowedRows]);
  // nextAutoId(): a page-wide counter shared by every Rozie component instance. It
  // lives on globalThis (read through Reflect, which type-checks in the plain-JS and
  // the TS script alike) so separately bundled copies of a leaf never hand out the
  // same id. The same four lines live in Combobox, Listbox and Popover.
  const nextAutoId = useCallback(() => {
    const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
    Reflect.set(globalThis, '__rozieAutoId', n);
    return n;
  }, []);
  // idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
  // pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
  function idRoot() {
    return props.id || autoId || 'rozie-listbox';
  }

  const _kickWindowRef = useRef(kickWindow);
  _kickWindowRef.current = kickWindow;
  const _syncRowsRef = useRef(syncRows);
  _syncRowsRef.current = syncRows;
  const _virtualizerOptionsRef = useRef(virtualizerOptions);
  _virtualizerOptionsRef.current = virtualizerOptions;
  useEffect(() => {
    if (!_idRef.current) setAutoId('rozie-listbox-' + nextAutoId());
    _syncRowsRef.current();
    if (_virtualRef.current) {
      // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
      // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
      // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
      // Solid/Lit, which leaves the virtualizer with no scroll element.
      gridScrollEl.current = __rozieRoot.current ? __rozieRoot.current!.querySelector('.rozie-listbox-list') : null;
      virtualizer.current = new Virtualizer(_virtualizerOptionsRef.current());
      virtualizerCleanup.current = virtualizer.current._didMount();
      setWindowVer(prev => prev + 1);
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => _kickWindowRef.current(8));else setTimeout(() => _kickWindowRef.current(8), 0);
    }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
  useEffect(() => {
    return () => {
      if (typeTimer.current !== null) clearTimeout(typeTimer.current);
      // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
      if (virtualizerCleanup.current) virtualizerCleanup.current();
    };
  }, []);
  useEffect(() => {
    if (_watch0First.current) { _watch0First.current = false; return; }
    syncRows();
    if (props.virtual && virtualizer.current) {
      gridScrollEl.current = __rozieRoot.current ? __rozieRoot.current!.querySelector('.rozie-listbox-list') : gridScrollEl.current;
      virtualizer.current.setOptions(virtualizerOptions());
      virtualizer.current._willUpdate();
      setWindowVer(prev => prev + 1);
      scheduleRemeasure();
    }
  }, [props.options, query]); // eslint-disable-line react-hooks/exhaustive-deps

  useOutsideClick(
    [controlEl, listEl],
    close,
    () => !!(open$local),
  );

  const _rozieExposeRef = useRef({ open, close, toggle, clear, focusControl });
  _rozieExposeRef.current = { open, close, toggle, clear, focusControl };
  useImperativeHandle(ref, () => ({ open: (...args: Parameters<typeof open>): ReturnType<typeof open> => _rozieExposeRef.current.open(...args), close: (...args: Parameters<typeof close>): ReturnType<typeof close> => _rozieExposeRef.current.close(...args), toggle: (...args: Parameters<typeof toggle>): ReturnType<typeof toggle> => _rozieExposeRef.current.toggle(...args), clear: (...args: Parameters<typeof clear>): ReturnType<typeof clear> => _rozieExposeRef.current.clear(...args), focusControl: (...args: Parameters<typeof focusControl>): ReturnType<typeof focusControl> => _rozieExposeRef.current.focusControl(...args) }), []);

  return (
    <>
    <div ref={__rozieRoot} {...attrs} className={clsx(clsx("rozie-listbox", { "rozie-listbox-open": open$local, "rozie-listbox-disabled": props.disabled, "rozie-listbox-inline": props.inline }), (attrs.className as string | undefined))} data-rozie-s-b576227a="">

      
      <div className={"rozie-listbox-control"} ref={controlEl} data-rozie-s-b576227a="">
        <button ref={triggerEl} type="button" className={"rozie-listbox-trigger"} role="combobox" aria-haspopup="listbox" aria-expanded={open$local} aria-controls={rozieAttr(idRoot() + '-list')} aria-activedescendant={rozieAttr(activeDescendant)} aria-label={rozieAttr(props.ariaLabel)} disabled={props.disabled} onClick={toggle} onKeyDown={($event) => { onControlKeyDown($event); }} data-rozie-s-b576227a="">
          {(props.renderSelected ?? props.slots?.['selected']) ? ((props.renderSelected ?? props.slots?.['selected']) as Function)({ selected: selectedLabel, value }) : ((selectedLabel) ? <span className={"rozie-listbox-selected"} data-rozie-s-b576227a="">{rozieDisplay(selectedLabel)}</span> : <span className={"rozie-listbox-placeholder"} data-rozie-s-b576227a="">{props.placeholder}</span>)}
          <span className={"rozie-listbox-arrow"} aria-hidden="true" data-rozie-s-b576227a="">▾</span>
        </button>
      </div>

      
      {!!(open$local && !props.virtual) && <div ref={listEl} className={"rozie-listbox-list"} role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={rozieAttr(props.ariaLabel)} aria-multiselectable={props.multiple} data-rozie-s-b576227a="">
        {visibleOptions().map((opt, index) => <div key={optionId(index)} id={rozieAttr(optionId(index))} className={clsx("rozie-listbox-option", { "is-active": activeIndex === index, "is-selected": isSelected(opt), "is-disabled": disabledOf(opt) })} role="option" aria-selected={!!isSelected(opt)} aria-disabled={!!disabledOf(opt)} onClick={($event) => { select(opt); }} onMouseMove={($event) => { onOptionPointerMove(index); }} data-rozie-s-b576227a="">
          {(props.renderOption ?? props.slots?.['option']) ? ((props.renderOption ?? props.slots?.['option']) as Function)({ option: opt, index, active: activeIndex === index, selected: isSelected(opt), disabled: disabledOf(opt) }) : (rozieDisplay(labelOf(opt)))}
        </div>)}

        {!!(visibleOptions().length === 0) && <div className={"rozie-listbox-empty"} role="presentation" data-rozie-s-b576227a="">
          {(props.renderEmpty ?? props.slots?.['empty']) ? ((props.renderEmpty ?? props.slots?.['empty']) as Function)({ query }) : "No options"}
        </div>}</div>}{!!(props.virtual) && <div ref={listEl} className={"rozie-listbox-list rozie-listbox-list--virtual"} role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={rozieAttr(props.ariaLabel)} aria-multiselectable={props.multiple} style={parseInlineStyle((open$local ? '' : 'display:none;') + (props.maxHeight ? 'height:' + props.maxHeight + ';max-height:' + props.maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + props.maxHeight : 'overflow-y:auto'))} data-rozie-s-b576227a="">
        <div className={"rozie-listbox-spacer"} aria-hidden="true" style={parseInlineStyle('height:' + padTop() + 'px')} data-rozie-s-b576227a="" />

        {windowedRows().map((wr) => <div key={wr.row.id} id={rozieAttr(optionId(wr.vi.index))} data-index={rozieAttr(wr.vi.index)} className={clsx("rozie-listbox-option", { "is-active": activeIndex === wr.vi.index, "is-selected": isSelected(wr.row._opt), "is-disabled": disabledOf(wr.row._opt) })} role="option" aria-selected={!!isSelected(wr.row._opt)} aria-disabled={!!disabledOf(wr.row._opt)} onClick={($event) => { select(wr.row._opt); }} onMouseMove={($event) => { onOptionPointerMove(wr.vi.index); }} data-rozie-s-b576227a="">
          {(props.renderOption ?? props.slots?.['option']) ? ((props.renderOption ?? props.slots?.['option']) as Function)({ option: wr.row._opt, index: wr.vi.index, active: activeIndex === wr.vi.index, selected: isSelected(wr.row._opt), disabled: disabledOf(wr.row._opt) }) : (rozieDisplay(labelOf(wr.row._opt)))}
        </div>)}

        <div className={"rozie-listbox-spacer"} aria-hidden="true" style={parseInlineStyle('height:' + padBottom() + 'px')} data-rozie-s-b576227a="" />

        {!!(windowSource().length === 0) && <div className={"rozie-listbox-empty"} role="presentation" data-rozie-s-b576227a="">
          {(props.renderEmpty ?? props.slots?.['empty']) ? ((props.renderEmpty ?? props.slots?.['empty']) as Function)({ query }) : "No options"}
        </div>}</div>}</div>
    </>
  );
});
export default Listbox;
vue
<template>

<div :class="['rozie-listbox', { 'rozie-listbox-open': open$local, 'rozie-listbox-disabled': props.disabled, 'rozie-listbox-inline': props.inline }]" ref="__rozieRootRef" v-bind="$attrs">

  
  <div class="rozie-listbox-control" ref="controlElRef">
    <button ref="triggerElRef" type="button" class="rozie-listbox-trigger" role="combobox" aria-haspopup="listbox" :aria-expanded="(open$local) ?? undefined" :aria-controls="idRoot() + '-list'" :aria-activedescendant="(activeDescendant) ?? undefined" :aria-label="props.ariaLabel" :disabled="props.disabled" @click="toggle" @keydown="onControlKeyDown($event)">
      <slot name="selected" :selected="selectedLabel" :value="value">
        <span v-if="selectedLabel" class="rozie-listbox-selected">{{ selectedLabel }}</span><span v-else class="rozie-listbox-placeholder">{{ props.placeholder }}</span></slot>
      <span class="rozie-listbox-arrow" aria-hidden="true">▾</span>
    </button>
  </div>

  
  <div v-if="open$local && !props.virtual" ref="listElRef" class="rozie-listbox-list" role="listbox" :id="idRoot() + '-list'" :aria-label="props.ariaLabel" :aria-multiselectable="(props.multiple) ?? undefined">
    <div v-for="(opt, index) in visibleOptions()" :key="optionId(index)" :id="optionId(index)" :class="['rozie-listbox-option', { 'is-active': activeIndex === index, 'is-selected': isSelected(opt), 'is-disabled': disabledOf(opt) }]" role="option" :aria-selected="!!isSelected(opt)" :aria-disabled="!!disabledOf(opt)" @click="select(opt)" @mousemove="onOptionPointerMove(index)">
      <slot name="option" :option="opt" :index="index" :active="activeIndex === index" :selected="isSelected(opt)" :disabled="disabledOf(opt)">
        {{ labelOf(opt) }}
      </slot>
    </div>

    <div v-if="visibleOptions().length === 0" class="rozie-listbox-empty" role="presentation">
      <slot name="empty" :query="query">No options</slot>
    </div></div><div v-if="props.virtual" ref="listElRef" class="rozie-listbox-list rozie-listbox-list--virtual" role="listbox" :id="idRoot() + '-list'" :aria-label="props.ariaLabel" :aria-multiselectable="(props.multiple) ?? undefined" :style="(open$local ? '' : 'display:none;') + (props.maxHeight ? 'height:' + props.maxHeight + ';max-height:' + props.maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + props.maxHeight : 'overflow-y:auto')">
    <div class="rozie-listbox-spacer" aria-hidden="true" :style="'height:' + padTop() + 'px'"></div>

    <div v-for="wr in windowedRows()" :key="wr.row.id" :id="optionId(wr.vi.index)" :data-index="wr.vi.index" :class="['rozie-listbox-option', { 'is-active': activeIndex === wr.vi.index, 'is-selected': isSelected(wr.row._opt), 'is-disabled': disabledOf(wr.row._opt) }]" role="option" :aria-selected="!!isSelected(wr.row._opt)" :aria-disabled="!!disabledOf(wr.row._opt)" @click="select(wr.row._opt)" @mousemove="onOptionPointerMove(wr.vi.index)">
      <slot name="option" :option="wr.row._opt" :index="wr.vi.index" :active="activeIndex === wr.vi.index" :selected="isSelected(wr.row._opt)" :disabled="disabledOf(wr.row._opt)">
        {{ labelOf(wr.row._opt) }}
      </slot>
    </div>

    <div class="rozie-listbox-spacer" aria-hidden="true" :style="'height:' + padBottom() + 'px'"></div>

    <div v-if="windowSource().length === 0" class="rozie-listbox-empty" role="presentation">
      <slot name="empty" :query="query">No options</slot>
    </div></div></div>

</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useOutsideClick } from '@rozie/runtime-vue';

const props = withDefaults(
  defineProps<{
    /**
     * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
     */
    options?: any[];
    /**
     * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
     */
    multiple?: boolean;
    /**
     * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
     */
    inline?: boolean;
    /**
     * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
     */
    disabled?: boolean;
    /**
     * Placeholder text shown in the empty control.
     */
    placeholder?: string;
    /**
     * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
     */
    closeOnSelect?: boolean;
    /**
     * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
     */
    optionLabel?: ((...args: any[]) => any) | null;
    /**
     * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
     */
    optionValue?: ((...args: any[]) => any) | null;
    /**
     * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
     */
    optionDisabled?: ((...args: any[]) => any) | null;
    /**
     * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
     */
    id?: string;
    /**
     * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
     */
    ariaLabel?: string | null;
    /**
     * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
     */
    virtual?: boolean;
    /**
     * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
     */
    estimateRowHeight?: number;
    /**
     * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
     */
    maxHeight?: string;
  }>(),
  { options: () => [], multiple: false, inline: false, disabled: false, placeholder: '', closeOnSelect: true, optionLabel: null, optionValue: null, optionDisabled: null, id: '', ariaLabel: null, virtual: false, estimateRowHeight: 36, maxHeight: '' }
);

/**
 * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
 * @example
 * <Listbox v-model:value="fruit" :options="fruits" />
 */
const value = defineModel<unknown | null>('value', { default: null });

const emit = defineEmits<{
  'open-change': [...args: any[]];
  change: [...args: any[]];
}>();

defineSlots<{
  selected(props: { selected: any; value: any }): any;
  option(props: { option: any; index: any; active: any; selected: any; disabled: any }): any;
  empty(props: { query: any }): any;
  option(props: { option: any; index: any; active: any; selected: any; disabled: any }): any;
  empty(props: { query: any }): any;
}>();

const autoId = ref('');
const open$local = ref(false);
const activeIndex = ref(-1);
const query = ref('');
const rows = ref<any[]>([]);
const windowVer = ref(0);
const editVer = ref(0);

const controlElRef = ref<HTMLElement>();
const triggerElRef = ref<HTMLButtonElement>();
const listElRef = ref<HTMLElement>();
const __rozieRootRef = ref<HTMLElement>();

const selectedLabel = computed(() => {
  const cur = value.value;
  if (props.multiple) {
    // Read the model value into a local before narrowing: `$props.value` lowers
    // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
    // separate calls — narrowing one stable local works on every target.
    const arr = Array.isArray(cur) ? cur : [];
    if (arr.length === 0) return '';
    return props.options.filter((o: any) => arr.includes(valueOf(o))).map(labelOf).join(', ');
  }
  const match = props.options.find((o: any) => valueOf(o) === cur);
  return match === undefined ? '' : labelOf(match);
});
const activeDescendant = computed(() => {
  if (!open$local.value || activeIndex.value < 0) return null;
  return optionId(activeIndex.value);
});

// Type-ahead buffer for the select-only listbox trigger. Module-scope
// `let`s reassigned from handlers → the React emitter hoists them to `useRef`
// so they persist across renders (the setup-once guarantee); no-op elsewhere.
// They STAY in this host (not the shared spine) per the A==B rule: reassigned
// module-`let`s + sigils live in the host; the partial only closes over them.
let typeBuffer = '';
let typeTimer: any = null;
// ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
// Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
// partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
// navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
// selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
// script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
// IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
//
// ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
// The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
// windowing.rzts uses) along two axes:
//   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
//     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
//     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
//     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
//     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
//     activedescendant model.
//   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
//     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
//     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
//     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
//     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
//     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
//
// ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
//   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
//     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
//     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
//     over them (in `onTypeahead`).
//   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
//     generates in $onMount); `optionId` below derives every option id from it.
//   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
//     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
//     HOST functions; this partial only closes over them (it reads NO refs itself).
//   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
//     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
//     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
//     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
//     `$data.query`), NOT a discriminant prop.

// ---- option resolvers --------------------------------------------------
const labelOf = (opt: any) => {
  if (props.optionLabel !== null) return props.optionLabel(opt);
  if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
  return String(opt);
};
const valueOf = (opt: any) => {
  if (props.optionValue !== null) return props.optionValue(opt);
  if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
  return opt;
};
const disabledOf = (opt: any) => {
  if (props.optionDisabled !== null) return !!props.optionDisabled(opt);
  if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
  return false;
};
// `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
// per-instance id) so the option ids follow the host's auto-id fallback.
const optionId = (index: any) => idRoot() + '-opt-' + index;
// ---- derived state -----------------------------------------------------
// The visible option list: identity in select-only / non-filtering mode,
// a case-insensitive substring filter when a combobox query is present.
// A plain function (not `$computed`) so it reads uniformly across all six
// targets — a `$computed` is a value on React but an accessor on Solid, so
// aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
// plain function is identical everywhere.
const visibleOptions = () => {
  const q = (query.value || '').trim().toLowerCase();
  if (q === '') return props.options;
  return props.options.filter((opt: any) => labelOf(opt).toLowerCase().includes(q));
};

// The label shown in the (select-only) trigger when closed. A real `$computed`
// — read bare in the template, never aliased in script, so the per-target
// accessor form stays uniform.
// Is a given option currently selected? Multi compares array membership.
const isSelected = (opt: any) => {
  const v = valueOf(opt);
  const cur = value.value;
  if (props.multiple) return Array.isArray(cur) && cur.includes(v);
  return cur === v;
};
// First enabled visible index, preferring the currently-selected option.
const resolveInitialActive = () => {
  const opts = visibleOptions();
  const sel = opts.findIndex((o: any) => isSelected(o) && !disabledOf(o));
  if (sel !== -1) return sel;
  return opts.findIndex((o: any) => !disabledOf(o));
};
// ---- open / close ------------------------------------------------------
// Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
// $emit's directly — no longer funneled through a single wrapper. The
// former "route every emit through ONE wrapper fn" workaround guarded
// against a React duplicate `const {onOpenChange}=props` per emit-site
// (TS2451); verified against the current emitter (target-react
// `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
// hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
// target into exactly one destructure, and a non-escaping function (e.g.
// `open`, reachable here only via `$expose`) never destructures at all — so
// no combination of these three functions can produce the duplicate-const
// shape. See project_next_port_listbox / project_emitter_hardening_backlog.
const open = () => {
  if (props.disabled) return;
  if (open$local.value) return;
  open$local.value = true;
  activeIndex.value = resolveInitialActive();
  emit('open-change', {
    open: true
  });
};
const close = () => {
  if (!open$local.value) return;
  open$local.value = false;
  activeIndex.value = -1;
  emit('open-change', {
    open: false
  });
};
const toggle = () => {
  if (open$local.value) close();else open();
};
// ---- selection ---------------------------------------------------------
const select = (opt: any) => {
  if (disabledOf(opt)) return;
  const v = valueOf(opt);
  if (props.multiple) {
    const cur = value.value;
    const arr = Array.isArray(cur) ? cur : [];
    // Fresh array on every commit — in-place mutation is dropped by the
    // React/Solid/Lit/Angular change detectors.
    const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
    value.value = next;
    emit('change', {
      value: next,
      option: opt
    });
  } else {
    value.value = v;
    emit('change', {
      value: v,
      option: opt
    });
    if (props.closeOnSelect) {
      close();
      focusControl();
    }
  }
};
const clear = () => {
  const empty = props.multiple ? [] : null;
  value.value = empty;
  query.value = '';
  emit('change', {
    value: empty,
    option: null
  });
};
// ---- keyboard navigation over the VISIBLE list -------------------------
const nextEnabled = (from: any, dir: any) => {
  const opts = visibleOptions();
  if (opts.length === 0) return -1;
  let i = from;
  for (let step = 0; step < opts.length; step++) {
    i += dir;
    if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
    if (!disabledOf(opts[i])) return i;
  }
  return from;
};
const move = (dir: any) => {
  if (!open$local.value) {
    open();
    return;
  }
  const start = activeIndex.value < 0 ? dir > 0 ? -1 : 0 : activeIndex.value;
  activeIndex.value = nextEnabled(start, dir);
  scrollActiveIntoView();
};
const moveEdge = (toEnd: any) => {
  if (!open$local.value) open();
  activeIndex.value = toEnd ? nextEnabled(-1, -1) : nextEnabled(-1, 1);
  scrollActiveIntoView();
};
const commitActive = () => {
  const opts = visibleOptions();
  if (activeIndex.value >= 0 && activeIndex.value < opts.length) select(opts[activeIndex.value]);
};
// Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
// first option whose label starts with the buffer.
const onTypeahead = (ch: any) => {
  if (typeTimer !== null) clearTimeout(typeTimer);
  typeBuffer += ch.toLowerCase();
  typeTimer = setTimeout(() => {
    typeBuffer = '';
  }, 600);
  const opts = visibleOptions();
  const idx = opts.findIndex((o: any) => !disabledOf(o) && labelOf(o).toLowerCase().startsWith(typeBuffer));
  if (idx !== -1) {
    if (!open$local.value) open();
    activeIndex.value = idx;
    scrollActiveIntoView();
  }
};
// Key handler shared by the trigger and the combobox input. The printable-
// character branch is reached only in select-only mode (the combobox input
// types through @input).
const onControlKeyDown = ($event: any) => {
  const key = $event.key;
  if (key === 'ArrowDown') {
    $event.preventDefault();
    move(1);
  } else if (key === 'ArrowUp') {
    $event.preventDefault();
    move(-1);
  } else if (key === 'Home') {
    $event.preventDefault();
    moveEdge(false);
  } else if (key === 'End') {
    $event.preventDefault();
    moveEdge(true);
  } else if (key === 'Enter') {
    if (open$local.value) {
      $event.preventDefault();
      commitActive();
    }
  } else if (key === 'Escape') {
    if (open$local.value) {
      $event.preventDefault();
      close();
      focusControl();
    }
  } else if (key === ' ' || key === 'Spacebar') {
    // Space toggles / commits in a select-only host (a button trigger). A
    // filter-input host types the literal space into its <input> and does NOT
    // route Space through this reducer, so this branch is select-only by use.
    $event.preventDefault();
    if (!open$local.value) open();else commitActive();
  } else if (key === 'Tab') {
    if (open$local.value) close();
  } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
    onTypeahead(key);
  }
};

// Combobox input handler: keep the popup open while typing, reset the active
// highlight to the first match, and surface the query for remote filtering.
// Pointer hover sets the virtual highlight (matches native <select> feel).
const onOptionPointerMove = (index: any) => {
  if (activeIndex.value !== index) activeIndex.value = index;
};
// ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
// Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
// holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
// in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
// compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
//
// HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
// by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
//   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
//                             returns its pre-pagination row model, listbox/combobox return the
//                             filtered options). This partial MUST NOT reach into the host data engine
//                             directly — rows arrive ONLY through windowSource().
//   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
//   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
//   - gridScrollEl              — the scroll-container element handle.
//   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
//   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
//   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
//   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
//                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
//                             listbox passes nothing. Routing pinning through this host hook (NOT
//                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
//   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
//                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
//                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
//                             convention exactly as they already call `pinnedEditIndex()`.
//   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
//                             host until it defines the real column-axis mechanism (87-04+).
//   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
//                             no default.
//   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
//                             from table-core's `getSize()` under D-06. REQUIRED, no default.
//   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
//                             DataTable host unions pinned + active-cell + editing column indices into
//                             the column-window slice; listbox/combobox pass an empty array (host-
//                             provided, defaulting to `[]`).
//   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
//                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
//   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
//                             data-table's real body reads `$props.autoMeasure === true`; listbox/
//                             combobox/command-palette return `false` so the accumulator branch
//                             estimateRowSize() gates on is dead code for them (D-20).
//   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
//                             assigned to refineRowEstimate() (below) by the host. The DataTable
//                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
//                             measureElement sweep so the fold + hysteresis re-feed run on every
//                             window commit. Routed through a mutable `let` rather than a direct
//                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
//                             calling a bare-specifier-partial CONST is the exact forward-reference
//                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
//                             documents for columnVirtualizerOptions() (inlineScriptPartials()
//                             groups the relative partial BEFORE the bare-specifier partial in the
//                             merged per-target output, regardless of source import order). A
//                             mutable `let` hoists to `useRef` on React and is excluded from a
//                             useCallback's dependency array, sidestepping the hazard entirely — the
//                             SAME mechanism `refreshRowModel` already relies on.
//
// AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
// LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
// note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
// (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
// `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
// `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
// (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
// (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
// (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
// (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
// axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
// sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
// Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
// (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
// hardcoded constant, separate from the row axis's `overscan: 8` below).
//
// isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
// immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
// signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
// mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.

// getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
// React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
// id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
const virtualItemKey = (i: any) => {
  const src = windowSource();
  return src && src[i] ? src[i].id : undefined;
};

// COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
// row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
// cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
// overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
// horizontal Virtualizer instance (see the AXIS MECHANISM note above).
// ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
// mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
// hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
// `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
// precedent), so two DataTable instances on one page never share an accumulator
// (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
// row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
// virtual-core (the hysteresis comparison baseline). ══
let measuredRowTotal = 0;
let measuredRowCount = 0;
// ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
// write behind a SINGLE microtask-deferred increment, regardless of how many callers request
// one within the same synchronous JS task. ══
//
// ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
// confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
// (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
// `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
// TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
// framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
// (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
// ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
// synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
// measured rows from the flat seed to their real heights) can fire onChange, and therefore an
// UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
// refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
// folded those same measurements into the running mean or re-fed the converged estimate into
// virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
// getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
// resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
// reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
// effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
// resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
// refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
// reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
// synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
// correct by construction. Solid does not auto-batch a signal write made from outside a
// Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
// externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
// virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
// getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
// the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
// recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
// further row happens to differ from its cache — so the DOM stays frozen on that stale,
// pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
// converged correctly (T-87-07's own confirmed finding).
//
// FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
// refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
// idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
// EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
// fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
// batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
// column widths come from table-core's getSize() oracle, never measured from the DOM), so
// columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
let windowVerBumpPending = false;
const bumpWindowVer = (): void => {
  if (windowVerBumpPending) return;
  windowVerBumpPending = true;
  const flush = () => {
    windowVerBumpPending = false;
    windowVer.value = windowVer.value + 1;
  };
  // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
  // (virtualization.rzts) for environments where queueMicrotask is unavailable.
  if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
};

// ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
// virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
// re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
// own measureElement/resizeItem idempotence (see refineRowEstimate() below).
// estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
// FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
// verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
// regardless of autoMeasure) still returns the seed — the very first render has nothing
// measured yet either way (D-15).
const estimateRowSize = (i: number): number => {
  if (!autoMeasureOn()) return props.estimateRowHeight;
  if (measuredRowCount === 0) return props.estimateRowHeight;
  return Math.round(measuredRowTotal / measuredRowCount);
};

// foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
// accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
// The FULL virtualizer options. virtual-core's setOptions REPLACES options with
// `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
// source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
// Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
// on virtual-core's generic option inference. onChange's windowVer write is routed through
// bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
// this onChange MANY times in a single synchronous sweep (once per row whose real measured
// size differs from its cache, e.g. every never-before-measured row in the FIRST window),
// and coalescing those into one microtask-deferred write is what keeps every target's render
// landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
// Solid-specific rendering gap this closes). The React emitter still lowers the underlying
// `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
// microtask, exactly as it was correct from a mount closure before.
const virtualizerOptions = (): any => ({
  count: windowSource().length,
  getScrollElement: () => gridScrollEl,
  estimateSize: (i: any) => estimateRowSize(i),
  observeElementRect,
  observeElementOffset,
  scrollToFn: elementScroll,
  measureElement,
  overscan: 8,
  getItemKey: virtualItemKey,
  onChange: () => {
    bumpWindowVer();
    // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
    // virtual-core only observe()s a node you explicitly hand to measureElement (it does
    // NOT auto-discover rendered rows — measureElement is the SOLE caller of
    // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
    // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
    // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
    // so the new <tr> set is in the DOM before we measure. Safe from an infinite
    // measure→onChange→measure loop: measureElement is idempotent on an already-observed
    // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
    // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
    // re-measure is a no-op.
    scheduleRemeasure();
  }
});
// pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
// shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
// two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
// no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
// `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
// guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
// thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
// gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
// Typing-only: the runtime value (a measurement or null) is unchanged.
const pinMeasurement = (pin: number): {
  start: number;
  size: number;
  index: number;
  end: number;
} | null => pinnedMeasurement(pin);
// windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
// { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
// $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
// full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
// `rows` binding → TS2448 self-shadow, line ~1149 lesson).
const windowedRows = () => {
  // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
  // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
  // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
  // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
  // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
  // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
  // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
  // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
  // first re-run that picks up the now-non-null virtualizer.
  // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
  // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
  void windowVer.value;
  void editVer.value;
  if (!virtualizer) {
    // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
    // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
    // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
    // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
    // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
    // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
    // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
    // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
    // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
    // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
    // the first onChange after _didMount.
    if (!rowsWindowed()) {
      const rowList = rows.value || [];
      return rowList.map((r: any, i: any) => ({
        vi: {
          index: i
        },
        row: r
      }));
    }
    return [];
  }
  const items = virtualizer.getVirtualItems();
  const rowList = rows.value || [];
  // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
  // shrink window where the virtualizer count is stale relative to $data.rows on the async
  // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
  // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
  const out = items.map((vi: any) => ({
    vi,
    row: rowList[vi.index]
  })).filter((wr: any) => wr.row);
  // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
  // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
  // into another full-model row), LEADING the slice when it sits above the window and TRAILING
  // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
  // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
  // into the real windowing.
  const pin = pinnedEditIndex();
  if (pin >= 0 && rowList[pin]) {
    let inWindow = false;
    for (let i = 0; i < items.length; i++) {
      if (items[i].index === pin) {
        inWindow = true;
        break;
      }
    }
    if (!inWindow) {
      const pm = pinMeasurement(pin);
      const firstStart = items.length ? items[0].start : 0;
      const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
      const pinnedEntry = {
        vi: pm != null ? pm : {
          index: pin
        },
        row: rowList[pin],
        pinned: true
      };
      if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
    }
  }
  return out;
};
// Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
// the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
// (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
const padTop = () => {
  // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
  // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
  // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
  void windowVer.value;
  void editVer.value;
  if (!rowsWindowed() || !virtualizer) return 0;
  const items = virtualizer.getVirtualItems();
  let pad = items.length ? items[0].start : 0;
  // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
  // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
  // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
  const pin = pinnedEditIndex();
  if (pin >= 0) {
    const pm = pinMeasurement(pin);
    const inWindow = pmIndexInWindow(items, pin);
    if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
  }
  return pad < 0 ? 0 : pad;
};
const padBottom = () => {
  // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
  // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
  // on pin/unpin.
  void windowVer.value;
  void editVer.value;
  if (!rowsWindowed() || !virtualizer) return 0;
  const items = virtualizer.getVirtualItems();
  if (!items.length) return 0;
  let pad = virtualizer.getTotalSize() - items[items.length - 1].end;
  // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
  // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
  const pin = pinnedEditIndex();
  if (pin >= 0) {
    const pm = pinMeasurement(pin);
    const inWindow = pmIndexInWindow(items, pin);
    // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
    // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
    // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
    // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
    // the offset comparison only if the measurement lacks an index (defensive).
    const lastItemIdx = items[items.length - 1].index;
    const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
    if (pm && !inWindow && below) {
      // below the window → it trailed the slice; subtract its height from the trailing spacer.
      if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
    }
  }
  return pad < 0 ? 0 : pad;
};
// pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
const pmIndexInWindow = (items: any, idx: any) => {
  for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
  return false;
};
// rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
// window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
const rowIsOutsideWindow = (r: any) => {
  if (!rowsWindowed() || !virtualizer) return false;
  const items = virtualizer.getVirtualItems();
  for (const it of items as any) if (it.index === r) return false;
  return true;
};

// ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
// rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
// a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
// returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
// cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══

// windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';
// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.
let virtualizer: any = null;
let virtualizerCleanup: any = null;
let gridScrollEl: any = null;
let remeasurePending = false;
// Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
// the option count at that moment, and the last scrollTop already accounted for.
let scrollEndPinned = false;
let scrollEndPinnedCount = -1;
let scrollEndPinnedTop = -1;

// windowSource(): the windowing.rzts host-contract row source — the FILTERED option
// set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
// (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
// wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
// — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
// every framework :key (and every virtual-core measurement key) to `undefined`, which
// recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
// row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
// value, `_opt` the original option (read via wr.row._opt in the windowed template),
// `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
// the same wrapped row the count windows over.
//
// $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
// FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
// would never hit while filtering). The key covers everything the map reads:
// options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
// resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
// Same reference-stability contract the sibling Combobox's filteredOptions carries —
// virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
// unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
const windowSourceCache = {
  keys: null as any[] | null,
  val: null as any
};
const windowSource = () => {
  const __rozieMemoKey = [props.options, query.value, props.optionValue, props.optionLabel];
  const __rozieMemoPrev = windowSourceCache.keys;
  if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
    return windowSourceCache.val;
  }
  const __rozieMemoVal = visibleOptions().map((o: any, i: any) => ({
    id: valueOf(o),
    _opt: o,
    _i: i
  }));
  windowSourceCache.keys = __rozieMemoKey;
  windowSourceCache.val = __rozieMemoVal;
  return __rozieMemoVal;
};
// D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
// A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
// pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
// reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
// to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
const pinnedEditIndex = () => -1;
const pinnedMeasurement = (pin: any) => null;
// D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
// today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
// windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
// `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
// pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
// in this plan calls these through a type-narrowing wrapper.
//
// GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
// `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
// alongside this were REMOVED here — they were dead code shipped on a mistaken premise
// about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
// virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
// from windowing.rzts; none of those functions' bodies reference the column-axis symbols
// (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
// `colIsOutsideWindow()` do, and Listbox never imports any of those), so
// `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
// 87-16-SUMMARY.md for the verification trail.
const rowsWindowed = () => !!props.virtual;
// autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
// gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
// estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
// column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
// — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
// `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
const autoMeasureOn = () => false;
// Keep $data.rows === windowSource() so the windowing math indexes the live option set.
const syncRows = () => {
  rows.value = windowSource();
};
// SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
// variable-height list at the end while the options in view measure taller than their
// estimate. The view is judged on the DOM, and only at a move the USER made — a move is
// virtual-core's own when it still holds an unreconciled scroll adjustment
// (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
// computed from its last-observed (stale) offset, so it pulls the view back up from the end
// and must neither clear the pin nor be mistaken for the user leaving the end. That position
// is remembered so the scroll event that later reports it is not read as a user move either.
// (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
// with every option measured in the ResizeObserver batch while its offset stays at the stale
// value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
const recordScrollEnd = () => {
  if (!virtualizer || !gridScrollEl || virtualizer.scrollState) return;
  const top = gridScrollEl.scrollTop;
  if (top === scrollEndPinnedTop) return;
  scrollEndPinnedTop = top;
  if (virtualizer.scrollAdjustments !== 0) return;
  // Only a list that actually overflows has an end to hold: while the window has not painted
  // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
  // recorded then would jump the freshly opened list to the bottom.
  const sh = gridScrollEl.scrollHeight;
  const ch = gridScrollEl.clientHeight;
  scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
  scrollEndPinnedCount = windowSource().length;
};
// Re-apply the pin after the framework has committed the window (called from the rAF pass):
// the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
// flight, and not when the option count changed since the user reached the end (a new filter
// or appended options must not be auto-followed).
const keepScrollEnd = () => {
  if (!scrollEndPinned || !virtualizer || !gridScrollEl || virtualizer.scrollState) return;
  if (windowSource().length !== scrollEndPinnedCount) return;
  const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight;
  if (maxTop - gridScrollEl.scrollTop > 1) {
    gridScrollEl.scrollTop = maxTop;
    scrollEndPinnedTop = gridScrollEl.scrollTop;
  }
};
// Defer remeasureWindow() until AFTER the framework commits the recycled window
// (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
// behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
// per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
// SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
// budget), the rAF catches React's async commit. measureElement is idempotent on an
// already-observed node, so running both is cheap and loop-free.
const scheduleRemeasure = () => {
  recordScrollEnd();
  if (remeasurePending) return;
  remeasurePending = true;
  let ranMicro = false;
  const microPass = () => {
    remeasureWindow();
  };
  // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
  // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
  // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
  // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
  // committed options cover the virtualizer's window, bounded (the data-table host twin).
  let rafAttempts = 0;
  const rafPass = () => {
    const covered = remeasureWindow();
    rafAttempts = rafAttempts + 1;
    if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(rafPass);
      return;
    }
    keepScrollEnd();
    remeasurePending = false;
  };
  if (typeof queueMicrotask !== 'undefined') {
    ranMicro = true;
    queueMicrotask(microPass);
  }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) remeasurePending = false;else setTimeout(rafPass, 0);
};
// measureElement sweep: hand every rendered windowed option to the virtualizer so its
// true (variable) height is observed (virtual-core measures ONLY nodes passed to
// measureElement, keyed by the data-index attribute). Bails during a programmatic
// scroll (scrollToIndex) so a measure can't starve the scroll target.
const remeasureWindow = () => {
  if (!virtualizer || !gridScrollEl) return true;
  if (virtualizer.scrollState) return true;
  const els = gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]');
  const rendered = new Set();
  for (const el of els as any) {
    virtualizer.measureElement(el);
    rendered.add(el.getAttribute('data-index'));
  }
  // N-05: false while the framework has not yet committed the recycled window.
  const items = virtualizer.getVirtualItems();
  for (let i = 0; i < items.length; i++) {
    if (!rendered.has(String(items[i].index))) return false;
  }
  return true;
};
// ---- focus / scroll helpers (post-mount $refs only) --------------------
// Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
// only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
// verb would override the inherited HTMLElement.focus method on the Lit element.
const focusControl = () => {
  triggerElRef.value?.focus();
};
// Keep the active option visible inside the scrolling listbox. Reads $refs in
// a post-mount callback only (never eagerly — ROZ123). When windowing, route through
// the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
// scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
const scrollActiveIntoView = () => {
  if (activeIndex.value < 0) return;
  if (props.virtual && virtualizer) {
    // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
    // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
    // leave it just-unrendered for a frame on the fine-grained targets (Solid).
    virtualizer.scrollToIndex(activeIndex.value, {
      align: 'center'
    });
    scheduleRemeasure();
    return;
  }
  if (!listElRef.value) return;
  const el = listElRef.value!.querySelector('#' + CSS.escape(optionId(activeIndex.value)));
  el?.scrollIntoView({
    block: 'nearest'
  });
};
// ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
// kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
// re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
// (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
// re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
// asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
// (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
// scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
// (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
// paints (or attempts run out) — idempotent + loop-free.
const kickWindow = (attempts: any) => {
  if (!virtualizer) return;
  gridScrollEl = __rozieRootRef.value ? __rozieRootRef.value!.querySelector('.rozie-listbox-list') : gridScrollEl;
  // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
  // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
  // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
  // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
  if (windowSource().length > 0) {
    syncRows();
    virtualizer.setOptions(virtualizerOptions());
  }
  virtualizer._willUpdate();
  windowVer.value = windowVer.value + 1;
  remeasureWindow();
  if (windowedRows().length === 0 && attempts > 0) {
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(attempts - 1));else setTimeout(() => kickWindow(attempts - 1), 16);
  }
};
// nextAutoId(): a page-wide counter shared by every Rozie component instance. It
// lives on globalThis (read through Reflect, which type-checks in the plain-JS and
// the TS script alike) so separately bundled copies of a leaf never hand out the
// same id. The same four lines live in Combobox, Listbox and Popover.
const nextAutoId = () => {
  const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
  Reflect.set(globalThis, '__rozieAutoId', n);
  return n;
};
// idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
// pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
const idRoot = () => props.id || autoId.value || 'rozie-listbox';

onMounted(() => {
  if (!props.id) autoId.value = 'rozie-listbox-' + nextAutoId();
  syncRows();
  if (props.virtual) {
    // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
    // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
    // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
    // Solid/Lit, which leaves the virtualizer with no scroll element.
    gridScrollEl = __rozieRootRef.value ? __rozieRootRef.value!.querySelector('.rozie-listbox-list') : null;
    virtualizer = new Virtualizer(virtualizerOptions());
    virtualizerCleanup = virtualizer._didMount();
    windowVer.value = windowVer.value + 1;
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(8));else setTimeout(() => kickWindow(8), 0);
  }
});
onBeforeUnmount(() => {
  if (typeTimer !== null) clearTimeout(typeTimer);
  // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
  if (virtualizerCleanup) virtualizerCleanup();
});

watch(() => (props.options ? props.options.length : 0) + '|' + query.value, () => {
  syncRows();
  if (props.virtual && virtualizer) {
    gridScrollEl = __rozieRootRef.value ? __rozieRootRef.value!.querySelector('.rozie-listbox-list') : gridScrollEl;
    virtualizer.setOptions(virtualizerOptions());
    virtualizer._willUpdate();
    windowVer.value = windowVer.value + 1;
    scheduleRemeasure();
  }
}, { flush: 'post' });

defineExpose({ open, close, toggle, clear, focusControl });

useOutsideClick(
  [controlElRef, listElRef],
  ($event) => ((close) as (...args: any[]) => any)($event),
  () => open$local.value,
);
</script>

<style scoped>
.rozie-listbox {
  position: relative;
  display: inline-block;
  min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
  font: var(--rozie-listbox-font, inherit);
}
.rozie-listbox-control { display: block; }
.rozie-listbox-input,
.rozie-listbox-trigger {
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
  padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
  font: inherit;
  text-align: left;
  background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
  border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
  cursor: pointer;
}
.rozie-listbox-input { cursor: text; }
.rozie-listbox-input:focus-visible,
.rozie-listbox-input:focus,
.rozie-listbox-trigger:focus-visible,
.rozie-listbox-trigger:focus {
  outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
  outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
}
.rozie-listbox-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
.rozie-listbox-placeholder { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
.rozie-listbox-arrow {
  font-size: 0.75em;
  color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
  opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
}
.rozie-listbox-list {
  position: absolute;
  z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
  top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
  max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
  overflow-y: auto;
  list-style: none;
  background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
  border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
  box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
}
.rozie-listbox-inline {
  display: block;
  width: 100%;
}
.rozie-listbox-inline .rozie-listbox-list {
  position: static;
  margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.rozie-listbox-option {
  padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
  border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
  color: var(--rozie-listbox-option-fg, inherit);
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
}
.rozie-listbox-option.is-active {
  background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
  color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
}
.rozie-listbox-option.is-selected {
  background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
  color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
  font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
}
.rozie-listbox-option.is-selected::after {
  content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
  color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
}
.rozie-listbox-option.is-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
.rozie-listbox-empty { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
.rozie-listbox-spacer { margin: 0; padding: 0; border: 0; flex: none; }
.rozie-listbox-list--virtual { overflow-anchor: none; }
</style>
svelte
<script lang="ts">
import { applyListeners, rozieAttr, rozieDisplay, rozieStyle } from '@rozie/runtime-svelte';

import type { Snippet } from 'svelte';
import { onDestroy, onMount, untrack } from 'svelte';

interface Props extends Omit<import('svelte/elements').SvelteHTMLElements['div'], 'options' | 'value' | 'multiple' | 'inline' | 'disabled' | 'placeholder' | 'closeOnSelect' | 'optionLabel' | 'optionValue' | 'optionDisabled' | 'id' | 'ariaLabel' | 'virtual' | 'estimateRowHeight' | 'maxHeight' | 'selected' | 'option' | 'empty' | 'snippets' | 'onopenchange' | 'onchange' | 'children'> {
  /**
   * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
   */
  options?: any[];
  /**
   * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
   * @example
   * <Listbox bind:value={fruit} options={fruits} />
   */
  value?: (unknown) | null;
  /**
   * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
   */
  multiple?: boolean;
  /**
   * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
   */
  inline?: boolean;
  /**
   * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
   */
  disabled?: boolean;
  /**
   * Placeholder text shown in the empty control.
   */
  placeholder?: string;
  /**
   * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
   */
  closeOnSelect?: boolean;
  /**
   * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
   */
  optionLabel?: ((...args: any[]) => any) | null;
  /**
   * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
   */
  optionValue?: ((...args: any[]) => any) | null;
  /**
   * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
   */
  optionDisabled?: ((...args: any[]) => any) | null;
  /**
   * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
   */
  id?: string;
  /**
   * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
   */
  ariaLabel?: (string) | null;
  /**
   * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
   */
  virtual?: boolean;
  /**
   * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
   */
  estimateRowHeight?: number;
  /**
   * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
   */
  maxHeight?: string;
  selected?: Snippet<[{ selected: any; value: any }]>;
  option?: Snippet<[{ option: any; index: any; active: any; selected: any; disabled: any }]>;
  empty?: Snippet<[{ query: any }]>;
  snippets?: Record<string, any>;
  onopenchange?: (...args: any[]) => void;
  onchange?: (...args: any[]) => void;
}

let __defaultOptions = (() => [])();

let {
  options = __defaultOptions,
  value = $bindable(null),
  multiple = false,
  inline = false,
  disabled = false,
  placeholder = '',
  closeOnSelect = true,
  optionLabel = null,
  optionValue = null,
  optionDisabled = null,
  id = '',
  ariaLabel = null,
  virtual = false,
  estimateRowHeight = 36,
  maxHeight = '',
  selected: __selectedProp,
  option: __optionProp,
  empty: __emptyProp,
  snippets,
  onopenchange,
  onchange,
  ...__rozieAttrs
}: Props = $props();

const selected = $derived(__selectedProp ?? snippets?.selected);
const option = $derived(__optionProp ?? snippets?.option);
const empty = $derived(__emptyProp ?? snippets?.empty);

let autoId = $state('');
let open$local = $state(false);
let activeIndex = $state(-1);
let query = $state('');
let rows: any[] = $state([]);
let windowVer = $state(0);
let editVer = $state(0);

let controlEl = $state<HTMLElement | undefined>(undefined);
let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
let listEl = $state<HTMLElement | undefined>(undefined);
let __rozieRoot = $state<HTMLElement | undefined>(undefined);

// Type-ahead buffer for the select-only listbox trigger. Module-scope
// `let`s reassigned from handlers → the React emitter hoists them to `useRef`
// so they persist across renders (the setup-once guarantee); no-op elsewhere.
// They STAY in this host (not the shared spine) per the A==B rule: reassigned
// module-`let`s + sigils live in the host; the partial only closes over them.
let typeBuffer = '';
let typeTimer: any = null;
// ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
// Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
// partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
// navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
// selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
// script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
// IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
//
// ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
// The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
// windowing.rzts uses) along two axes:
//   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
//     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
//     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
//     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
//     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
//     activedescendant model.
//   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
//     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
//     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
//     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
//     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
//     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
//
// ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
//   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
//     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
//     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
//     over them (in `onTypeahead`).
//   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
//     generates in $onMount); `optionId` below derives every option id from it.
//   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
//     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
//     HOST functions; this partial only closes over them (it reads NO refs itself).
//   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
//     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
//     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
//     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
//     `$data.query`), NOT a discriminant prop.

// ---- option resolvers --------------------------------------------------
const labelOf = (opt: any) => {
  if (optionLabel !== null) return optionLabel(opt);
  if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
  return String(opt);
};
const valueOf = (opt: any) => {
  if (optionValue !== null) return optionValue(opt);
  if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
  return opt;
};
const disabledOf = (opt: any) => {
  if (optionDisabled !== null) return !!optionDisabled(opt);
  if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
  return false;
};
// `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
// per-instance id) so the option ids follow the host's auto-id fallback.
const optionId = (index: any) => idRoot() + '-opt-' + index;
// ---- derived state -----------------------------------------------------
// The visible option list: identity in select-only / non-filtering mode,
// a case-insensitive substring filter when a combobox query is present.
// A plain function (not `$computed`) so it reads uniformly across all six
// targets — a `$computed` is a value on React but an accessor on Solid, so
// aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
// plain function is identical everywhere.
const visibleOptions = () => {
  const q = (query || '').trim().toLowerCase();
  if (q === '') return options;
  return options.filter((opt: any) => labelOf(opt).toLowerCase().includes(q));
};

// The label shown in the (select-only) trigger when closed. A real `$computed`
// — read bare in the template, never aliased in script, so the per-target
// accessor form stays uniform.
// Is a given option currently selected? Multi compares array membership.
const isSelected = (opt: any) => {
  const v = valueOf(opt);
  const cur = value;
  if (multiple) return Array.isArray(cur) && cur.includes(v);
  return cur === v;
};
// First enabled visible index, preferring the currently-selected option.
const resolveInitialActive = () => {
  const opts = visibleOptions();
  const sel = opts.findIndex((o: any) => isSelected(o) && !disabledOf(o));
  if (sel !== -1) return sel;
  return opts.findIndex((o: any) => !disabledOf(o));
};
// ---- open / close ------------------------------------------------------
// Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
// $emit's directly — no longer funneled through a single wrapper. The
// former "route every emit through ONE wrapper fn" workaround guarded
// against a React duplicate `const {onOpenChange}=props` per emit-site
// (TS2451); verified against the current emitter (target-react
// `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
// hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
// target into exactly one destructure, and a non-escaping function (e.g.
// `open`, reachable here only via `$expose`) never destructures at all — so
// no combination of these three functions can produce the duplicate-const
// shape. See project_next_port_listbox / project_emitter_hardening_backlog.
export const open = () => {
  if (disabled) return;
  if (open$local) return;
  open$local = true;
  activeIndex = resolveInitialActive();
  onopenchange?.({
    open: true
  });
};
export const close = () => {
  if (!open$local) return;
  open$local = false;
  activeIndex = -1;
  onopenchange?.({
    open: false
  });
};
export const toggle = () => {
  if (open$local) close();else open();
};
// ---- selection ---------------------------------------------------------
const select = (opt: any) => {
  if (disabledOf(opt)) return;
  const v = valueOf(opt);
  if (multiple) {
    const cur = value;
    const arr = Array.isArray(cur) ? cur : [];
    // Fresh array on every commit — in-place mutation is dropped by the
    // React/Solid/Lit/Angular change detectors.
    const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
    value = next;
    onchange?.({
      value: next,
      option: opt
    });
  } else {
    value = v;
    onchange?.({
      value: v,
      option: opt
    });
    if (closeOnSelect) {
      close();
      focusControl();
    }
  }
};
export const clear = () => {
  const empty = multiple ? [] : null;
  value = empty;
  query = '';
  onchange?.({
    value: empty,
    option: null
  });
};
// ---- keyboard navigation over the VISIBLE list -------------------------
const nextEnabled = (from: any, dir: any) => {
  const opts = visibleOptions();
  if (opts.length === 0) return -1;
  let i = from;
  for (let step = 0; step < opts.length; step++) {
    i += dir;
    if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
    if (!disabledOf(opts[i])) return i;
  }
  return from;
};
const move = (dir: any) => {
  if (!open$local) {
    open();
    return;
  }
  const start = activeIndex < 0 ? dir > 0 ? -1 : 0 : activeIndex;
  activeIndex = nextEnabled(start, dir);
  scrollActiveIntoView();
};
const moveEdge = (toEnd: any) => {
  if (!open$local) open();
  activeIndex = toEnd ? nextEnabled(-1, -1) : nextEnabled(-1, 1);
  scrollActiveIntoView();
};
const commitActive = () => {
  const opts = visibleOptions();
  if (activeIndex >= 0 && activeIndex < opts.length) select(opts[activeIndex]);
};
// Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
// first option whose label starts with the buffer.
const onTypeahead = (ch: any) => {
  if (typeTimer !== null) clearTimeout(typeTimer);
  typeBuffer += ch.toLowerCase();
  typeTimer = setTimeout(() => {
    typeBuffer = '';
  }, 600);
  const opts = visibleOptions();
  const idx = opts.findIndex((o: any) => !disabledOf(o) && labelOf(o).toLowerCase().startsWith(typeBuffer));
  if (idx !== -1) {
    if (!open$local) open();
    activeIndex = idx;
    scrollActiveIntoView();
  }
};
// Key handler shared by the trigger and the combobox input. The printable-
// character branch is reached only in select-only mode (the combobox input
// types through @input).
const onControlKeyDown = ($event: any) => {
  const key = $event.key;
  if (key === 'ArrowDown') {
    $event.preventDefault();
    move(1);
  } else if (key === 'ArrowUp') {
    $event.preventDefault();
    move(-1);
  } else if (key === 'Home') {
    $event.preventDefault();
    moveEdge(false);
  } else if (key === 'End') {
    $event.preventDefault();
    moveEdge(true);
  } else if (key === 'Enter') {
    if (open$local) {
      $event.preventDefault();
      commitActive();
    }
  } else if (key === 'Escape') {
    if (open$local) {
      $event.preventDefault();
      close();
      focusControl();
    }
  } else if (key === ' ' || key === 'Spacebar') {
    // Space toggles / commits in a select-only host (a button trigger). A
    // filter-input host types the literal space into its <input> and does NOT
    // route Space through this reducer, so this branch is select-only by use.
    $event.preventDefault();
    if (!open$local) open();else commitActive();
  } else if (key === 'Tab') {
    if (open$local) close();
  } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
    onTypeahead(key);
  }
};

// Combobox input handler: keep the popup open while typing, reset the active
// highlight to the first match, and surface the query for remote filtering.
// Pointer hover sets the virtual highlight (matches native <select> feel).
const onOptionPointerMove = (index: any) => {
  if (activeIndex !== index) activeIndex = index;
};
// ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
// Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
// holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
// in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
// compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
//
// HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
// by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
//   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
//                             returns its pre-pagination row model, listbox/combobox return the
//                             filtered options). This partial MUST NOT reach into the host data engine
//                             directly — rows arrive ONLY through windowSource().
//   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
//   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
//   - gridScrollEl              — the scroll-container element handle.
//   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
//   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
//   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
//   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
//                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
//                             listbox passes nothing. Routing pinning through this host hook (NOT
//                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
//   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
//                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
//                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
//                             convention exactly as they already call `pinnedEditIndex()`.
//   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
//                             host until it defines the real column-axis mechanism (87-04+).
//   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
//                             no default.
//   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
//                             from table-core's `getSize()` under D-06. REQUIRED, no default.
//   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
//                             DataTable host unions pinned + active-cell + editing column indices into
//                             the column-window slice; listbox/combobox pass an empty array (host-
//                             provided, defaulting to `[]`).
//   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
//                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
//   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
//                             data-table's real body reads `$props.autoMeasure === true`; listbox/
//                             combobox/command-palette return `false` so the accumulator branch
//                             estimateRowSize() gates on is dead code for them (D-20).
//   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
//                             assigned to refineRowEstimate() (below) by the host. The DataTable
//                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
//                             measureElement sweep so the fold + hysteresis re-feed run on every
//                             window commit. Routed through a mutable `let` rather than a direct
//                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
//                             calling a bare-specifier-partial CONST is the exact forward-reference
//                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
//                             documents for columnVirtualizerOptions() (inlineScriptPartials()
//                             groups the relative partial BEFORE the bare-specifier partial in the
//                             merged per-target output, regardless of source import order). A
//                             mutable `let` hoists to `useRef` on React and is excluded from a
//                             useCallback's dependency array, sidestepping the hazard entirely — the
//                             SAME mechanism `refreshRowModel` already relies on.
//
// AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
// LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
// note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
// (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
// `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
// `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
// (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
// (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
// (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
// (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
// axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
// sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
// Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
// (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
// hardcoded constant, separate from the row axis's `overscan: 8` below).
//
// isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
// immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
// signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
// mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.

// getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
// React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
// id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
const virtualItemKey = (i: any) => {
  const src = windowSource();
  return src && src[i] ? src[i].id : undefined;
};

// COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
// row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
// cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
// overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
// horizontal Virtualizer instance (see the AXIS MECHANISM note above).
// ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
// mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
// hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
// `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
// precedent), so two DataTable instances on one page never share an accumulator
// (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
// row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
// virtual-core (the hysteresis comparison baseline). ══
let measuredRowTotal = 0;
let measuredRowCount = 0;
// ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
// write behind a SINGLE microtask-deferred increment, regardless of how many callers request
// one within the same synchronous JS task. ══
//
// ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
// confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
// (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
// `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
// TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
// framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
// (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
// ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
// synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
// measured rows from the flat seed to their real heights) can fire onChange, and therefore an
// UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
// refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
// folded those same measurements into the running mean or re-fed the converged estimate into
// virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
// getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
// resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
// reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
// effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
// resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
// refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
// reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
// synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
// correct by construction. Solid does not auto-batch a signal write made from outside a
// Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
// externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
// virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
// getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
// the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
// recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
// further row happens to differ from its cache — so the DOM stays frozen on that stale,
// pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
// converged correctly (T-87-07's own confirmed finding).
//
// FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
// refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
// idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
// EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
// fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
// batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
// column widths come from table-core's getSize() oracle, never measured from the DOM), so
// columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
let windowVerBumpPending = false;
const bumpWindowVer = (): void => {
  if (windowVerBumpPending) return;
  windowVerBumpPending = true;
  const flush = () => {
    windowVerBumpPending = false;
    windowVer = windowVer + 1;
  };
  // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
  // (virtualization.rzts) for environments where queueMicrotask is unavailable.
  if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
};

// ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
// virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
// re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
// own measureElement/resizeItem idempotence (see refineRowEstimate() below).
// estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
// FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
// verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
// regardless of autoMeasure) still returns the seed — the very first render has nothing
// measured yet either way (D-15).
const estimateRowSize = (i: number): number => {
  if (!autoMeasureOn()) return estimateRowHeight;
  if (measuredRowCount === 0) return estimateRowHeight;
  return Math.round(measuredRowTotal / measuredRowCount);
};

// foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
// accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
// The FULL virtualizer options. virtual-core's setOptions REPLACES options with
// `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
// source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
// Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
// on virtual-core's generic option inference. onChange's windowVer write is routed through
// bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
// this onChange MANY times in a single synchronous sweep (once per row whose real measured
// size differs from its cache, e.g. every never-before-measured row in the FIRST window),
// and coalescing those into one microtask-deferred write is what keeps every target's render
// landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
// Solid-specific rendering gap this closes). The React emitter still lowers the underlying
// `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
// microtask, exactly as it was correct from a mount closure before.
const virtualizerOptions = (): any => ({
  count: windowSource().length,
  getScrollElement: () => gridScrollEl,
  estimateSize: (i: any) => estimateRowSize(i),
  observeElementRect,
  observeElementOffset,
  scrollToFn: elementScroll,
  measureElement,
  overscan: 8,
  getItemKey: virtualItemKey,
  onChange: () => {
    bumpWindowVer();
    // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
    // virtual-core only observe()s a node you explicitly hand to measureElement (it does
    // NOT auto-discover rendered rows — measureElement is the SOLE caller of
    // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
    // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
    // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
    // so the new <tr> set is in the DOM before we measure. Safe from an infinite
    // measure→onChange→measure loop: measureElement is idempotent on an already-observed
    // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
    // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
    // re-measure is a no-op.
    scheduleRemeasure();
  }
});
// pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
// shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
// two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
// no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
// `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
// guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
// thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
// gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
// Typing-only: the runtime value (a measurement or null) is unchanged.
const pinMeasurement = (pin: number): {
  start: number;
  size: number;
  index: number;
  end: number;
} | null => pinnedMeasurement(pin);
// windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
// { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
// $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
// full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
// `rows` binding → TS2448 self-shadow, line ~1149 lesson).
const windowedRows = () => {
  // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
  // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
  // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
  // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
  // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
  // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
  // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
  // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
  // first re-run that picks up the now-non-null virtualizer.
  // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
  // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
  void windowVer;
  void editVer;
  if (!virtualizer) {
    // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
    // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
    // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
    // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
    // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
    // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
    // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
    // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
    // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
    // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
    // the first onChange after _didMount.
    if (!rowsWindowed()) {
      const rowList = rows || [];
      return rowList.map((r: any, i: any) => ({
        vi: {
          index: i
        },
        row: r
      }));
    }
    return [];
  }
  const items = virtualizer.getVirtualItems();
  const rowList = rows || [];
  // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
  // shrink window where the virtualizer count is stale relative to $data.rows on the async
  // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
  // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
  const out = items.map((vi: any) => ({
    vi,
    row: rowList[vi.index]
  })).filter((wr: any) => wr.row);
  // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
  // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
  // into another full-model row), LEADING the slice when it sits above the window and TRAILING
  // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
  // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
  // into the real windowing.
  const pin = pinnedEditIndex();
  if (pin >= 0 && rowList[pin]) {
    let inWindow = false;
    for (let i = 0; i < items.length; i++) {
      if (items[i].index === pin) {
        inWindow = true;
        break;
      }
    }
    if (!inWindow) {
      const pm = pinMeasurement(pin);
      const firstStart = items.length ? items[0].start : 0;
      const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
      const pinnedEntry = {
        vi: pm != null ? pm : {
          index: pin
        },
        row: rowList[pin],
        pinned: true
      };
      if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
    }
  }
  return out;
};
// Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
// the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
// (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
const padTop = () => {
  // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
  // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
  // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
  void windowVer;
  void editVer;
  if (!rowsWindowed() || !virtualizer) return 0;
  const items = virtualizer.getVirtualItems();
  let pad = items.length ? items[0].start : 0;
  // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
  // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
  // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
  const pin = pinnedEditIndex();
  if (pin >= 0) {
    const pm = pinMeasurement(pin);
    const inWindow = pmIndexInWindow(items, pin);
    if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
  }
  return pad < 0 ? 0 : pad;
};
const padBottom = () => {
  // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
  // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
  // on pin/unpin.
  void windowVer;
  void editVer;
  if (!rowsWindowed() || !virtualizer) return 0;
  const items = virtualizer.getVirtualItems();
  if (!items.length) return 0;
  let pad = virtualizer.getTotalSize() - items[items.length - 1].end;
  // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
  // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
  const pin = pinnedEditIndex();
  if (pin >= 0) {
    const pm = pinMeasurement(pin);
    const inWindow = pmIndexInWindow(items, pin);
    // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
    // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
    // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
    // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
    // the offset comparison only if the measurement lacks an index (defensive).
    const lastItemIdx = items[items.length - 1].index;
    const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
    if (pm && !inWindow && below) {
      // below the window → it trailed the slice; subtract its height from the trailing spacer.
      if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
    }
  }
  return pad < 0 ? 0 : pad;
};
// pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
const pmIndexInWindow = (items: any, idx: any) => {
  for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
  return false;
};
// rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
// window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
const rowIsOutsideWindow = (r: any) => {
  if (!rowsWindowed() || !virtualizer) return false;
  const items = virtualizer.getVirtualItems();
  for (const it of items as any) if (it.index === r) return false;
  return true;
};

// ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
// rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
// a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
// returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
// cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══

// windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';
// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.
let virtualizer: any = null;
let virtualizerCleanup: any = null;
let gridScrollEl: any = null;
let remeasurePending = false;
// Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
// the option count at that moment, and the last scrollTop already accounted for.
let scrollEndPinned = false;
let scrollEndPinnedCount = -1;
let scrollEndPinnedTop = -1;

// windowSource(): the windowing.rzts host-contract row source — the FILTERED option
// set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
// (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
// wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
// — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
// every framework :key (and every virtual-core measurement key) to `undefined`, which
// recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
// row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
// value, `_opt` the original option (read via wr.row._opt in the windowed template),
// `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
// the same wrapped row the count windows over.
//
// $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
// FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
// would never hit while filtering). The key covers everything the map reads:
// options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
// resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
// Same reference-stability contract the sibling Combobox's filteredOptions carries —
// virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
// unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
const windowSourceCache = {
  keys: null as any[] | null,
  val: null as any
};
const windowSource = () => {
  const __rozieMemoKey = [options, query, optionValue, optionLabel];
  const __rozieMemoPrev = windowSourceCache.keys;
  if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
    return windowSourceCache.val;
  }
  const __rozieMemoVal = visibleOptions().map((o: any, i: any) => ({
    id: valueOf(o),
    _opt: o,
    _i: i
  }));
  windowSourceCache.keys = __rozieMemoKey;
  windowSourceCache.val = __rozieMemoVal;
  return __rozieMemoVal;
};
// D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
// A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
// pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
// reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
// to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
const pinnedEditIndex = () => -1;
const pinnedMeasurement = (pin: any) => null;
// D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
// today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
// windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
// `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
// pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
// in this plan calls these through a type-narrowing wrapper.
//
// GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
// `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
// alongside this were REMOVED here — they were dead code shipped on a mistaken premise
// about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
// virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
// from windowing.rzts; none of those functions' bodies reference the column-axis symbols
// (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
// `colIsOutsideWindow()` do, and Listbox never imports any of those), so
// `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
// 87-16-SUMMARY.md for the verification trail.
const rowsWindowed = () => !!virtual;
// autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
// gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
// estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
// column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
// — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
// `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
const autoMeasureOn = () => false;
// Keep $data.rows === windowSource() so the windowing math indexes the live option set.
const syncRows = () => {
  rows = windowSource();
};
// SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
// variable-height list at the end while the options in view measure taller than their
// estimate. The view is judged on the DOM, and only at a move the USER made — a move is
// virtual-core's own when it still holds an unreconciled scroll adjustment
// (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
// computed from its last-observed (stale) offset, so it pulls the view back up from the end
// and must neither clear the pin nor be mistaken for the user leaving the end. That position
// is remembered so the scroll event that later reports it is not read as a user move either.
// (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
// with every option measured in the ResizeObserver batch while its offset stays at the stale
// value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
const recordScrollEnd = () => {
  if (!virtualizer || !gridScrollEl || virtualizer.scrollState) return;
  const top = gridScrollEl.scrollTop;
  if (top === scrollEndPinnedTop) return;
  scrollEndPinnedTop = top;
  if (virtualizer.scrollAdjustments !== 0) return;
  // Only a list that actually overflows has an end to hold: while the window has not painted
  // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
  // recorded then would jump the freshly opened list to the bottom.
  const sh = gridScrollEl.scrollHeight;
  const ch = gridScrollEl.clientHeight;
  scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
  scrollEndPinnedCount = windowSource().length;
};
// Re-apply the pin after the framework has committed the window (called from the rAF pass):
// the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
// flight, and not when the option count changed since the user reached the end (a new filter
// or appended options must not be auto-followed).
const keepScrollEnd = () => {
  if (!scrollEndPinned || !virtualizer || !gridScrollEl || virtualizer.scrollState) return;
  if (windowSource().length !== scrollEndPinnedCount) return;
  const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight;
  if (maxTop - gridScrollEl.scrollTop > 1) {
    gridScrollEl.scrollTop = maxTop;
    scrollEndPinnedTop = gridScrollEl.scrollTop;
  }
};
// Defer remeasureWindow() until AFTER the framework commits the recycled window
// (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
// behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
// per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
// SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
// budget), the rAF catches React's async commit. measureElement is idempotent on an
// already-observed node, so running both is cheap and loop-free.
const scheduleRemeasure = () => {
  recordScrollEnd();
  if (remeasurePending) return;
  remeasurePending = true;
  let ranMicro = false;
  const microPass = () => {
    remeasureWindow();
  };
  // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
  // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
  // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
  // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
  // committed options cover the virtualizer's window, bounded (the data-table host twin).
  let rafAttempts = 0;
  const rafPass = () => {
    const covered = remeasureWindow();
    rafAttempts = rafAttempts + 1;
    if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(rafPass);
      return;
    }
    keepScrollEnd();
    remeasurePending = false;
  };
  if (typeof queueMicrotask !== 'undefined') {
    ranMicro = true;
    queueMicrotask(microPass);
  }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) remeasurePending = false;else setTimeout(rafPass, 0);
};
// measureElement sweep: hand every rendered windowed option to the virtualizer so its
// true (variable) height is observed (virtual-core measures ONLY nodes passed to
// measureElement, keyed by the data-index attribute). Bails during a programmatic
// scroll (scrollToIndex) so a measure can't starve the scroll target.
const remeasureWindow = () => {
  if (!virtualizer || !gridScrollEl) return true;
  if (virtualizer.scrollState) return true;
  const els = gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]');
  const rendered = new Set();
  for (const el of els as any) {
    virtualizer.measureElement(el);
    rendered.add(el.getAttribute('data-index'));
  }
  // N-05: false while the framework has not yet committed the recycled window.
  const items = virtualizer.getVirtualItems();
  for (let i = 0; i < items.length; i++) {
    if (!rendered.has(String(items[i].index))) return false;
  }
  return true;
};
// ---- focus / scroll helpers (post-mount $refs only) --------------------
// Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
// only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
// verb would override the inherited HTMLElement.focus method on the Lit element.
export const focusControl = () => {
  triggerEl?.focus();
};
// Keep the active option visible inside the scrolling listbox. Reads $refs in
// a post-mount callback only (never eagerly — ROZ123). When windowing, route through
// the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
// scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
const scrollActiveIntoView = () => {
  if (activeIndex < 0) return;
  if (virtual && virtualizer) {
    // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
    // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
    // leave it just-unrendered for a frame on the fine-grained targets (Solid).
    virtualizer.scrollToIndex(activeIndex, {
      align: 'center'
    });
    scheduleRemeasure();
    return;
  }
  if (!listEl) return;
  const el = listEl!.querySelector('#' + CSS.escape(optionId(activeIndex)));
  el?.scrollIntoView({
    block: 'nearest'
  });
};
// ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
// kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
// re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
// (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
// re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
// asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
// (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
// scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
// (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
// paints (or attempts run out) — idempotent + loop-free.
const kickWindow = (attempts: any) => {
  if (!virtualizer) return;
  gridScrollEl = __rozieRoot ? __rozieRoot!.querySelector('.rozie-listbox-list') : gridScrollEl;
  // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
  // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
  // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
  // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
  if (windowSource().length > 0) {
    syncRows();
    virtualizer.setOptions(virtualizerOptions());
  }
  virtualizer._willUpdate();
  windowVer = windowVer + 1;
  remeasureWindow();
  if (windowedRows().length === 0 && attempts > 0) {
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(attempts - 1));else setTimeout(() => kickWindow(attempts - 1), 16);
  }
};
// nextAutoId(): a page-wide counter shared by every Rozie component instance. It
// lives on globalThis (read through Reflect, which type-checks in the plain-JS and
// the TS script alike) so separately bundled copies of a leaf never hand out the
// same id. The same four lines live in Combobox, Listbox and Popover.
const nextAutoId = () => {
  const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
  Reflect.set(globalThis, '__rozieAutoId', n);
  return n;
};
// idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
// pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
const idRoot = () => id || autoId || 'rozie-listbox';

const selectedLabel = $derived.by(() => {
  const cur = value;
  if (multiple) {
    // Read the model value into a local before narrowing: `$props.value` lowers
    // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
    // separate calls — narrowing one stable local works on every target.
    const arr = Array.isArray(cur) ? cur : [];
    if (arr.length === 0) return '';
    return options.filter((o: any) => arr.includes(valueOf(o))).map(labelOf).join(', ');
  }
  const match = options.find((o: any) => valueOf(o) === cur);
  return match === undefined ? '' : labelOf(match);
});
const activeDescendant = $derived.by(() => {
  if (!open$local || activeIndex < 0) return null;
  return optionId(activeIndex);
});

onMount(() => {
  if (!id) autoId = 'rozie-listbox-' + nextAutoId();
  syncRows();
  if (virtual) {
    // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
    // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
    // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
    // Solid/Lit, which leaves the virtualizer with no scroll element.
    gridScrollEl = __rozieRoot ? __rozieRoot!.querySelector('.rozie-listbox-list') : null;
    virtualizer = new Virtualizer(virtualizerOptions());
    virtualizerCleanup = virtualizer._didMount();
    windowVer = windowVer + 1;
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(8));else setTimeout(() => kickWindow(8), 0);
  }
});
onDestroy(() => (() => {
  if (typeTimer !== null) clearTimeout(typeTimer);
  // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
  if (virtualizerCleanup) virtualizerCleanup();
})());

let __rozieWatchInitial_0 = true;
$effect(() => { (() => (options ? options.length : 0) + '|' + query)(); untrack(() => { if (__rozieWatchInitial_0) { __rozieWatchInitial_0 = false; return; } (() => {
  syncRows();
  if (virtual && virtualizer) {
    gridScrollEl = __rozieRoot ? __rozieRoot!.querySelector('.rozie-listbox-list') : gridScrollEl;
    virtualizer.setOptions(virtualizerOptions());
    virtualizer._willUpdate();
    windowVer = windowVer + 1;
    scheduleRemeasure();
  }
})(); }); });

$effect(() => {
  if (!(open$local)) return;
  const handler = ($event: MouseEvent) => {
    const target = $event.target as Node;
    if (controlEl?.contains(target) || listEl?.contains(target)) return;
    ((close) as (...args: any[]) => any)($event);
  };
  let attached = false;
  let cancelled = false;
  const timer = setTimeout(() => {
    if (cancelled) return;
    document.addEventListener('click', handler, { capture: true });
    attached = true;
  }, 0);
  return () => {
    cancelled = true;
    clearTimeout(timer);
    if (attached) document.removeEventListener('click', handler, { capture: true });
  };
});
</script>

<div bind:this={__rozieRoot} {...__rozieAttrs} class={["rozie-listbox", { 'rozie-listbox-open': open$local, 'rozie-listbox-disabled': disabled, 'rozie-listbox-inline': inline }, (__rozieAttrs)?.class]} use:applyListeners={__rozieAttrs} data-rozie-s-b576227a><div class="rozie-listbox-control" bind:this={controlEl} data-rozie-s-b576227a><button bind:this={triggerEl} type="button" class="rozie-listbox-trigger" role="combobox" aria-haspopup="listbox" aria-expanded={open$local} aria-controls={rozieAttr(idRoot() + '-list')} aria-activedescendant={rozieAttr(activeDescendant)} aria-label={ariaLabel} disabled={disabled} onclick={toggle} onkeydown={($event) => { onControlKeyDown($event); }} data-rozie-s-b576227a>{#if selected}{@render selected({ selected: selectedLabel, value })}{:else}{#if selectedLabel}<span class="rozie-listbox-selected" data-rozie-s-b576227a>{rozieDisplay(selectedLabel)}</span>{:else}<span class="rozie-listbox-placeholder" data-rozie-s-b576227a>{placeholder}</span>{/if}{/if}<span class="rozie-listbox-arrow" aria-hidden="true" data-rozie-s-b576227a>▾</span></button></div>{#if open$local && !virtual}<div bind:this={listEl} class="rozie-listbox-list" role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={ariaLabel} aria-multiselectable={multiple} data-rozie-s-b576227a>{#each visibleOptions() as opt, index (optionId(index))}<div id={rozieAttr(optionId(index))} class={["rozie-listbox-option", { 'is-active': activeIndex === index, 'is-selected': isSelected(opt), 'is-disabled': disabledOf(opt) }]} role="option" aria-selected={!!isSelected(opt)} aria-disabled={!!disabledOf(opt)} onclick={($event) => { select(opt); }} onmousemove={($event) => { onOptionPointerMove(index); }} data-rozie-s-b576227a>{#if option}{@render option({ option: opt, index, active: activeIndex === index, selected: isSelected(opt), disabled: disabledOf(opt) })}{:else}{rozieDisplay(labelOf(opt))}{/if}</div>{/each}{#if visibleOptions().length === 0}<div class="rozie-listbox-empty" role="presentation" data-rozie-s-b576227a>{#if empty}{@render empty({ query })}{:else}No options{/if}</div>{/if}</div>{/if}{#if virtual}<div bind:this={listEl} class="rozie-listbox-list rozie-listbox-list--virtual" role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={ariaLabel} aria-multiselectable={multiple} style={rozieStyle((open$local ? '' : 'display:none;') + (maxHeight ? 'height:' + maxHeight + ';max-height:' + maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + maxHeight : 'overflow-y:auto'))} data-rozie-s-b576227a><div class="rozie-listbox-spacer" aria-hidden="true" style={rozieStyle('height:' + padTop() + 'px')} data-rozie-s-b576227a></div>{#each windowedRows() as wr (wr.row.id)}<div id={rozieAttr(optionId(wr.vi.index))} data-index={rozieAttr(wr.vi.index)} class={["rozie-listbox-option", { 'is-active': activeIndex === wr.vi.index, 'is-selected': isSelected(wr.row._opt), 'is-disabled': disabledOf(wr.row._opt) }]} role="option" aria-selected={!!isSelected(wr.row._opt)} aria-disabled={!!disabledOf(wr.row._opt)} onclick={($event) => { select(wr.row._opt); }} onmousemove={($event) => { onOptionPointerMove(wr.vi.index); }} data-rozie-s-b576227a>{#if option}{@render option({ option: wr.row._opt, index: wr.vi.index, active: activeIndex === wr.vi.index, selected: isSelected(wr.row._opt), disabled: disabledOf(wr.row._opt) })}{:else}{rozieDisplay(labelOf(wr.row._opt))}{/if}</div>{/each}<div class="rozie-listbox-spacer" aria-hidden="true" style={rozieStyle('height:' + padBottom() + 'px')} data-rozie-s-b576227a></div>{#if windowSource().length === 0}<div class="rozie-listbox-empty" role="presentation" data-rozie-s-b576227a>{#if empty}{@render empty({ query })}{:else}No options{/if}</div>{/if}</div>{/if}</div>

<style>
:global {
  .rozie-listbox[data-rozie-s-b576227a] {
    position: relative;
    display: inline-block;
    min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
    font: var(--rozie-listbox-font, inherit);
  }
  .rozie-listbox-control[data-rozie-s-b576227a] { display: block; }
  .rozie-listbox-input[data-rozie-s-b576227a],
  .rozie-listbox-trigger[data-rozie-s-b576227a] {
    box-sizing: border-box;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
    padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
    font: inherit;
    text-align: left;
    background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
    color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
    border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
    border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
    cursor: pointer;
  }
  .rozie-listbox-input[data-rozie-s-b576227a] { cursor: text; }
  .rozie-listbox-input[data-rozie-s-b576227a]:focus-visible,
  .rozie-listbox-input[data-rozie-s-b576227a]:focus,
  .rozie-listbox-trigger[data-rozie-s-b576227a]:focus-visible,
  .rozie-listbox-trigger[data-rozie-s-b576227a]:focus {
    outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
    outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
  }
  .rozie-listbox-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
  .rozie-listbox-placeholder[data-rozie-s-b576227a] { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
  .rozie-listbox-arrow[data-rozie-s-b576227a] {
    font-size: 0.75em;
    color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
    opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
  }
  .rozie-listbox-list[data-rozie-s-b576227a] {
    position: absolute;
    z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
    top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
    left: 0;
    right: 0;
    margin: 0;
    padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
    max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
    overflow-y: auto;
    list-style: none;
    background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
    color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
    border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
    border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
    box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
  }
  .rozie-listbox-inline[data-rozie-s-b576227a] {
    display: block;
    width: 100%;
  }
  .rozie-listbox-inline[data-rozie-s-b576227a] .rozie-listbox-list[data-rozie-s-b576227a] {
    position: static;
    margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
    border: none;
    border-radius: 0;
    box-shadow: none;
  }
  .rozie-listbox-option[data-rozie-s-b576227a] {
    padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
    border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
    color: var(--rozie-listbox-option-fg, inherit);
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
  }
  .rozie-listbox-option.is-active[data-rozie-s-b576227a] {
    background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
    color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
  }
  .rozie-listbox-option.is-selected[data-rozie-s-b576227a] {
    background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
    color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
    font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
  }
  .rozie-listbox-option.is-selected[data-rozie-s-b576227a]::after {
    content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
    color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
  }
  .rozie-listbox-option.is-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
  .rozie-listbox-empty[data-rozie-s-b576227a] { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
  .rozie-listbox-spacer[data-rozie-s-b576227a] { margin: 0; padding: 0; border: 0; flex: none; }
  .rozie-listbox-list--virtual[data-rozie-s-b576227a] { overflow-anchor: none; }
}
</style>
ts
import { Component, ContentChild, DestroyRef, ElementRef, Renderer2, TemplateRef, ViewEncapsulation, afterRenderEffect, computed, contentChildren, effect, forwardRef, inject, input, model, output, signal, untracked, viewChild } from '@angular/core';
import { NgClass, NgTemplateOutlet } from '@angular/common';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { RozieSlot, createRozieAttrApplier, createRozieHostAttrsReader, rozieAttr as __rozieAttr, rozieDisplay as __rozieDisplay } from '@rozie/runtime-angular';

// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';

// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.

interface SelectedCtx {
  $implicit: { selected: any; value: any };
  selected: any;
  value: any;
}

interface OptionCtx {
  $implicit: { option: any; index: any; active: any; selected: any; disabled: any };
  option: any;
  index: any;
  active: any;
  selected: any;
  disabled: any;
}

interface EmptyCtx {
  $implicit: { query: any };
  query: any;
}

@Component({
  selector: 'rozie-listbox',
  standalone: true,
  imports: [NgTemplateOutlet, NgClass],
  template: `

    <div class="rozie-listbox" [ngClass]="{ 'rozie-listbox-open': open$local(), 'rozie-listbox-disabled': (disabled() || this.__rozieCvaDisabled()), 'rozie-listbox-inline': inline() }" #__rozieRoot #rozieSpread_0 #rozieListenersTarget_1>

      
      <div class="rozie-listbox-control" #controlEl>
        <button #triggerEl type="button" class="rozie-listbox-trigger" role="combobox" aria-haspopup="listbox" [attr.aria-expanded]="open$local()" [attr.aria-controls]="rozieAttr(idRoot() + '-list')" [attr.aria-activedescendant]="rozieAttr(activeDescendant())" [attr.aria-label]="rozieAttr(ariaLabel())" [disabled]="(disabled() || this.__rozieCvaDisabled())" (click)="toggle()" (keydown)="onControlKeyDown($event)">
          @if ((selectedTpl ?? __rozieFillMap()['selected'] ?? templates()?.['selected'])) {
    <ng-container *ngTemplateOutlet="(selectedTpl ?? __rozieFillMap()['selected'] ?? templates()?.['selected']); context: { $implicit: { selected: selectedLabel(), value: value() }, selected: selectedLabel(), value: value() }" />
    } @else {

            @if (selectedLabel()) {
    <span class="rozie-listbox-selected">{{ rozieDisplay(selectedLabel()) }}</span>
    } @else {
    <span class="rozie-listbox-placeholder">{{ placeholder() }}</span>
    }
    }
          <span class="rozie-listbox-arrow" aria-hidden="true">▾</span>
        </button>
      </div>

      
      @if (open$local() && !virtual()) {
    <div #listEl class="rozie-listbox-list" role="listbox" [attr.id]="rozieAttr(idRoot() + '-list')" [attr.aria-label]="rozieAttr(ariaLabel())" [attr.aria-multiselectable]="multiple()">
        @for (opt of visibleOptions(); track optionId(index); let index = $index) {
    <div [attr.id]="rozieAttr(optionId(index))" class="rozie-listbox-option" [ngClass]="{ 'is-active': activeIndex() === index, 'is-selected': isSelected(opt), 'is-disabled': disabledOf(opt) }" role="option" [attr.aria-selected]="!!isSelected(opt)" [attr.aria-disabled]="!!disabledOf(opt)" (click)="select(opt)" (mousemove)="onOptionPointerMove(index)">
          @if ((optionTpl ?? __rozieFillMap()['option'] ?? templates()?.['option'])) {
    <ng-container *ngTemplateOutlet="(optionTpl ?? __rozieFillMap()['option'] ?? templates()?.['option']); context: { $implicit: { option: opt, index: index, active: activeIndex() === index, selected: isSelected(opt), disabled: disabledOf(opt) }, option: opt, index: index, active: activeIndex() === index, selected: isSelected(opt), disabled: disabledOf(opt) }" />
    } @else {

            {{ rozieDisplay(labelOf(opt)) }}
          
    }
        </div>
    }

        @if (visibleOptions().length === 0) {
    <div class="rozie-listbox-empty" role="presentation">
          @if ((emptyTpl ?? __rozieFillMap()['empty'] ?? templates()?.['empty'])) {
    <ng-container *ngTemplateOutlet="(emptyTpl ?? __rozieFillMap()['empty'] ?? templates()?.['empty']); context: { $implicit: { query: query() }, query: query() }" />
    } @else {
    No options
    }
        </div>
    }</div>
    }@if (virtual()) {
    <div #listEl class="rozie-listbox-list rozie-listbox-list--virtual" role="listbox" [attr.id]="rozieAttr(idRoot() + '-list')" [attr.aria-label]="rozieAttr(ariaLabel())" [attr.aria-multiselectable]="multiple()" [attr.style]="__style">
        <div class="rozie-listbox-spacer" aria-hidden="true" [attr.style]="'height:' + padTop() + 'px'"></div>

        @for (wr of windowedRows(); track wr.row.id) {
    <div [attr.id]="rozieAttr(optionId(wr.vi.index))" [attr.data-index]="rozieAttr(wr.vi.index)" class="rozie-listbox-option" [ngClass]="{ 'is-active': activeIndex() === wr.vi.index, 'is-selected': isSelected(wr.row._opt), 'is-disabled': disabledOf(wr.row._opt) }" role="option" [attr.aria-selected]="!!isSelected(wr.row._opt)" [attr.aria-disabled]="!!disabledOf(wr.row._opt)" (click)="select(wr.row._opt)" (mousemove)="onOptionPointerMove(wr.vi.index)">
          @if ((optionTpl ?? __rozieFillMap()['option'] ?? templates()?.['option'])) {
    <ng-container *ngTemplateOutlet="(optionTpl ?? __rozieFillMap()['option'] ?? templates()?.['option']); context: { $implicit: { option: wr.row._opt, index: wr.vi.index, active: activeIndex() === wr.vi.index, selected: isSelected(wr.row._opt), disabled: disabledOf(wr.row._opt) }, option: wr.row._opt, index: wr.vi.index, active: activeIndex() === wr.vi.index, selected: isSelected(wr.row._opt), disabled: disabledOf(wr.row._opt) }" />
    } @else {

            {{ rozieDisplay(labelOf(wr.row._opt)) }}
          
    }
        </div>
    }

        <div class="rozie-listbox-spacer" aria-hidden="true" [attr.style]="'height:' + padBottom() + 'px'"></div>

        @if (windowSource().length === 0) {
    <div class="rozie-listbox-empty" role="presentation">
          @if ((emptyTpl ?? __rozieFillMap()['empty'] ?? templates()?.['empty'])) {
    <ng-container *ngTemplateOutlet="(emptyTpl ?? __rozieFillMap()['empty'] ?? templates()?.['empty']); context: { $implicit: { query: query() }, query: query() }" />
    } @else {
    No options
    }
        </div>
    }</div>
    }</div>

  `,
  styles: [`
    :host(rozie-listbox) { display: contents; }
    .rozie-listbox {
      position: relative;
      display: inline-block;
      min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
      font: var(--rozie-listbox-font, inherit);
    }
    .rozie-listbox-control { display: block; }
    .rozie-listbox-input,
    .rozie-listbox-trigger {
      box-sizing: border-box;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
      padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
      font: inherit;
      text-align: left;
      background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
      color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
      border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
      border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
      cursor: pointer;
    }
    .rozie-listbox-input { cursor: text; }
    .rozie-listbox-input:focus-visible,
    .rozie-listbox-input:focus,
    .rozie-listbox-trigger:focus-visible,
    .rozie-listbox-trigger:focus {
      outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
      outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
    }
    .rozie-listbox-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
    .rozie-listbox-placeholder { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
    .rozie-listbox-arrow {
      font-size: 0.75em;
      color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
      opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
    }
    .rozie-listbox-list {
      position: absolute;
      z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
      top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
      left: 0;
      right: 0;
      margin: 0;
      padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
      max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
      overflow-y: auto;
      list-style: none;
      background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
      color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
      border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
      border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
      box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
    }
    .rozie-listbox-inline {
      display: block;
      width: 100%;
    }
    .rozie-listbox-inline .rozie-listbox-list {
      position: static;
      margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
      border: none;
      border-radius: 0;
      box-shadow: none;
    }
    .rozie-listbox-option {
      padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
      border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
      color: var(--rozie-listbox-option-fg, inherit);
      cursor: pointer;
      user-select: none;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
    }
    .rozie-listbox-option.is-active {
      background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
      color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
    }
    .rozie-listbox-option.is-selected {
      background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
      color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
      font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
    }
    .rozie-listbox-option.is-selected::after {
      content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
      color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
    }
    .rozie-listbox-option.is-disabled { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
    .rozie-listbox-empty { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
    .rozie-listbox-spacer { margin: 0; padding: 0; border: 0; flex: none; }
    .rozie-listbox-list--virtual { overflow-anchor: none; }
  `],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => Listbox),
      multi: true,
    },
  ],
  host: { '(focusout)': '__rozieCvaOnTouched()' },
})
export class Listbox {
  /**
   * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
   */
  options = input<any[]>((() => [])());
  /**
   * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
   * @example
   * <rozie-listbox [(value)]="fruit" [options]="fruits" />
   */
  value = model<(unknown) | null>(null);
  /**
   * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
   */
  multiple = input<boolean>(false);
  /**
   * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
   */
  inline = input<boolean>(false);
  /**
   * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
   */
  disabled = input<boolean>(false);
  /**
   * Placeholder text shown in the empty control.
   */
  placeholder = input<string>('');
  /**
   * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
   */
  closeOnSelect = input<boolean>(true);
  /**
   * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
   */
  optionLabel = input<((...args: any[]) => any) | null>(null);
  /**
   * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
   */
  optionValue = input<((...args: any[]) => any) | null>(null);
  /**
   * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
   */
  optionDisabled = input<((...args: any[]) => any) | null>(null);
  /**
   * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
   */
  id = input<string>('');
  /**
   * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
   */
  ariaLabel = input<(string) | null>(null);
  /**
   * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
   */
  virtual = input<boolean>(false);
  /**
   * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
   */
  estimateRowHeight = input<number>(36);
  /**
   * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
   */
  maxHeight = input<string>('');
  autoId = signal('');
  open$local = signal(false);
  activeIndex = signal(-1);
  query = signal('');
  rows = signal<any[]>([]);
  windowVer = signal(0);
  editVer = signal(0);
  controlEl = viewChild<ElementRef<HTMLDivElement>>('controlEl');
  triggerEl = viewChild<ElementRef<HTMLButtonElement>>('triggerEl');
  listEl = viewChild<ElementRef<HTMLDivElement>>('listEl');
  __rozieRoot = viewChild<ElementRef<HTMLDivElement>>('__rozieRoot');
  openChange = output<unknown>({ alias: 'open-change' });
  change = output<unknown>();
  @ContentChild('selected', { read: TemplateRef }) selectedTpl?: TemplateRef<SelectedCtx>;
  @ContentChild('option', { read: TemplateRef }) optionTpl?: TemplateRef<OptionCtx>;
  @ContentChild('empty', { read: TemplateRef }) emptyTpl?: TemplateRef<EmptyCtx>;
  templates = input<Record<string, TemplateRef<unknown>> | undefined>(undefined);
  __rozieFills = contentChildren(RozieSlot, { descendants: true });
  __rozieFillMap = computed(() => {
    const map = Object.create(null) as Record<string, TemplateRef<unknown>>;
    for (const f of this.__rozieFills()) {
      const k = f.rozieSlot();
      if (k == null) continue;
      if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
      map[k === '' ? 'defaultSlot' : k] = f.templateRef;
    }
    return map;
  });
  private __rozieWatchInitial_0 = true;

  constructor() {
      const renderer = inject(Renderer2);

      effect((onCleanup) => {
        if (!(this.open$local())) return;
        const listenTarget = typeof document === 'undefined' ? null : document;
        if (!listenTarget) return;
        const handler = ($event: MouseEvent) => {
          const target = $event.target as Node;
          if (this.controlEl()?.nativeElement?.contains(target) || this.listEl()?.nativeElement?.contains(target)) return;
          ((this.close) as (...args: any[]) => any)($event);
        };
        listenTarget.addEventListener('click', handler as EventListener, true);
        onCleanup(() => listenTarget.removeEventListener('click', handler as EventListener, true));
      });

    inject(DestroyRef).onDestroy(() => {
      if (this.typeTimer !== null) clearTimeout(this.typeTimer);
      // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
      if (this.virtualizerCleanup) this.virtualizerCleanup();
    });
    effect(() => { const __watchVal = (() => (this.options() ? this.options().length : 0) + '|' + this.query())(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => {
      this.syncRows();
      if (this.virtual() && this.virtualizer) {
        this.gridScrollEl = this.__rozieRoot()?.nativeElement ? this.__rozieRoot()!.nativeElement.querySelector('.rozie-listbox-list') : this.gridScrollEl;
        this.virtualizer.setOptions(this.virtualizerOptions());
        this.virtualizer._willUpdate();
        this.windowVer.set(this.windowVer() + 1);
        this.scheduleRemeasure();
      }
    })(); }); });
  }

  ngAfterViewInit() {
    if (!this.id()) this.autoId.set('rozie-listbox-' + this.nextAutoId());
    this.syncRows();
    if (this.virtual()) {
      // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
      // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
      // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
      // Solid/Lit, which leaves the virtualizer with no scroll element.
      this.gridScrollEl = this.__rozieRoot()?.nativeElement ? this.__rozieRoot()!.nativeElement.querySelector('.rozie-listbox-list') : null;
      this.virtualizer = new Virtualizer(this.virtualizerOptions());
      this.virtualizerCleanup = this.virtualizer._didMount();
      this.windowVer.set(this.windowVer() + 1);
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => this.kickWindow(8));else setTimeout(() => this.kickWindow(8), 0);
    }
  }

  selectedLabel = computed(() => {
    const __options = this.options();
    const cur = this.value();
    if (this.multiple()) {
      // Read the model value into a local before narrowing: `$props.value` lowers
      // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
      // separate calls — narrowing one stable local works on every target.
      const arr = Array.isArray(cur) ? cur : [];
      if (arr.length === 0) return '';
      return __options.filter((o: any) => arr.includes(this.valueOf$local(o))).map(this.labelOf).join(', ');
    }
    const match = __options.find((o: any) => this.valueOf$local(o) === cur);
    return match === undefined ? '' : this.labelOf(match);
  });
  activeDescendant = computed(() => {
    const __activeIndex = this.activeIndex();
    if (!this.open$local() || __activeIndex < 0) return null;
    return this.optionId(__activeIndex);
  });

  // Type-ahead buffer for the select-only listbox trigger. Module-scope
  // `let`s reassigned from handlers → the React emitter hoists them to `useRef`
  // so they persist across renders (the setup-once guarantee); no-op elsewhere.
  // They STAY in this host (not the shared spine) per the A==B rule: reassigned
  // module-`let`s + sigils live in the host; the partial only closes over them.
  typeBuffer = '';
  typeTimer: any = null;
  // ---- shared list spine (P2: @rozie-ui/headless-core/listCore.rzts) ------
  // The option resolvers, client filter, enabled-index navigation, the keyboard
  // reducer, type-ahead, single+multi selection, open/close state, and
  // activeDescendant derivation now live in the shared, focus-/input-mode
  // parameterized list spine. It is a compile-time `.rzts` script-partial: it
  // dissolves into this leaf via inlineScriptPartials() before IR lowering (zero
  // runtime dep). Listbox consumes it in focus-model `activedescendant` +
  // input-mode `select-only` + multi + type-ahead. The spine closes over this
  // host's pieces by convention: the reassigned module-`let`s typeBuffer/typeTimer
  // (above) and the impure ref fns focusControl/scrollActiveIntoView (below).
  // ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
  // Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
  // partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
  // navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
  // selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
  // script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
  // IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
  //
  // ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
  // The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
  // windowing.rzts uses) along two axes:
  //   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
  //     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
  //     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
  //     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
  //     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
  //     activedescendant model.
  //   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
  //     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
  //     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
  //     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
  //     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
  //     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
  //
  // ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
  //   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
  //     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
  //     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
  //     over them (in `onTypeahead`).
  //   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
  //     generates in $onMount); `optionId` below derives every option id from it.
  //   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
  //     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
  //     HOST functions; this partial only closes over them (it reads NO refs itself).
  //   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
  //     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
  //     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
  //     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
  //     `$data.query`), NOT a discriminant prop.
  // ---- option resolvers --------------------------------------------------
  labelOf = (opt: any) => {
    const __optionLabel = this.optionLabel();
    if (__optionLabel !== null) return __optionLabel(opt);
    if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
    return String(opt);
  };
  valueOf$local = (opt: any) => {
    const __optionValue = this.optionValue();
    if (__optionValue !== null) return __optionValue(opt);
    if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
    return opt;
  };
  disabledOf = (opt: any) => {
    const __optionDisabled = this.optionDisabled();
    if (__optionDisabled !== null) return !!__optionDisabled(opt);
    if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
    return false;
  };
  // `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
  // per-instance id) so the option ids follow the host's auto-id fallback.
  optionId = (index: any) => this.idRoot() + '-opt-' + index;
  // ---- derived state -----------------------------------------------------
  // The visible option list: identity in select-only / non-filtering mode,
  // a case-insensitive substring filter when a combobox query is present.
  // A plain function (not `$computed`) so it reads uniformly across all six
  // targets — a `$computed` is a value on React but an accessor on Solid, so
  // aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
  // plain function is identical everywhere.
  visibleOptions = () => {
    const __options = this.options();
    const q = (this.query() || '').trim().toLowerCase();
    if (q === '') return __options;
    return __options.filter((opt: any) => this.labelOf(opt).toLowerCase().includes(q));
  };
  // Is a given option currently selected? Multi compares array membership.
  isSelected = (opt: any) => {
    const v = this.valueOf$local(opt);
    const cur = this.value();
    if (this.multiple()) return Array.isArray(cur) && cur.includes(v);
    return cur === v;
  };
  // First enabled visible index, preferring the currently-selected option.
  resolveInitialActive = () => {
    const opts = this.visibleOptions();
    const sel = opts.findIndex((o: any) => this.isSelected(o) && !this.disabledOf(o));
    if (sel !== -1) return sel;
    return opts.findIndex((o: any) => !this.disabledOf(o));
  };
  // ---- open / close ------------------------------------------------------
  // Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
  // $emit's directly — no longer funneled through a single wrapper. The
  // former "route every emit through ONE wrapper fn" workaround guarded
  // against a React duplicate `const {onOpenChange}=props` per emit-site
  // (TS2451); verified against the current emitter (target-react
  // `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
  // hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
  // target into exactly one destructure, and a non-escaping function (e.g.
  // `open`, reachable here only via `$expose`) never destructures at all — so
  // no combination of these three functions can produce the duplicate-const
  // shape. See project_next_port_listbox / project_emitter_hardening_backlog.
  open = () => {
    if ((this.disabled() || this.__rozieCvaDisabled())) return;
    if (this.open$local()) return;
    this.open$local.set(true);
    this.activeIndex.set(this.resolveInitialActive());
    this.openChange.emit({
      open: true
    });
  };
  close = () => {
    if (!this.open$local()) return;
    this.open$local.set(false);
    this.activeIndex.set(-1);
    this.openChange.emit({
      open: false
    });
  };
  toggle = () => {
    if (this.open$local()) this.close();else this.open();
  };
  // ---- selection ---------------------------------------------------------
  select = (opt: any) => {
    if (this.disabledOf(opt)) return;
    const v = this.valueOf$local(opt);
    if (this.multiple()) {
      const cur = this.value();
      const arr = Array.isArray(cur) ? cur : [];
      // Fresh array on every commit — in-place mutation is dropped by the
      // React/Solid/Lit/Angular change detectors.
      const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
      this.value.set(next), this.__rozieCvaOnChange(next);
      this.change.emit({
        value: next,
        option: opt
      });
    } else {
      this.value.set(v), this.__rozieCvaOnChange(v);
      this.change.emit({
        value: v,
        option: opt
      });
      if (this.closeOnSelect()) {
        this.close();
        this.focusControl();
      }
    }
  };
  clear = () => {
    const empty = this.multiple() ? [] : null;
    this.value.set(empty), this.__rozieCvaOnChange(empty);
    this.query.set('');
    this.change.emit({
      value: empty,
      option: null
    });
  };
  // ---- keyboard navigation over the VISIBLE list -------------------------
  nextEnabled = (from: any, dir: any) => {
    const opts = this.visibleOptions();
    if (opts.length === 0) return -1;
    let i = from;
    for (let step = 0; step < opts.length; step++) {
      i += dir;
      if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
      if (!this.disabledOf(opts[i])) return i;
    }
    return from;
  };
  move = (dir: any) => {
    if (!this.open$local()) {
      this.open();
      return;
    }
    const start = this.activeIndex() < 0 ? dir > 0 ? -1 : 0 : this.activeIndex();
    this.activeIndex.set(this.nextEnabled(start, dir));
    this.scrollActiveIntoView();
  };
  moveEdge = (toEnd: any) => {
    if (!this.open$local()) this.open();
    this.activeIndex.set(toEnd ? this.nextEnabled(-1, -1) : this.nextEnabled(-1, 1));
    this.scrollActiveIntoView();
  };
  commitActive = () => {
    const __activeIndex = this.activeIndex();
    const opts = this.visibleOptions();
    if (__activeIndex >= 0 && __activeIndex < opts.length) this.select(opts[__activeIndex]);
  };
  // Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
  // first option whose label starts with the buffer.
  onTypeahead = (ch: any) => {
    if (this.typeTimer !== null) clearTimeout(this.typeTimer);
    this.typeBuffer += ch.toLowerCase();
    this.typeTimer = setTimeout(() => {
      this.typeBuffer = '';
    }, 600);
    const opts = this.visibleOptions();
    const idx = opts.findIndex((o: any) => !this.disabledOf(o) && this.labelOf(o).toLowerCase().startsWith(this.typeBuffer));
    if (idx !== -1) {
      if (!this.open$local()) this.open();
      this.activeIndex.set(idx);
      this.scrollActiveIntoView();
    }
  };
  // Key handler shared by the trigger and the combobox input. The printable-
  // character branch is reached only in select-only mode (the combobox input
  // types through @input).
  onControlKeyDown = ($event: any) => {
    const __open$local = this.open$local();
    const key = $event.key;
    if (key === 'ArrowDown') {
      $event.preventDefault();
      this.move(1);
    } else if (key === 'ArrowUp') {
      $event.preventDefault();
      this.move(-1);
    } else if (key === 'Home') {
      $event.preventDefault();
      this.moveEdge(false);
    } else if (key === 'End') {
      $event.preventDefault();
      this.moveEdge(true);
    } else if (key === 'Enter') {
      if (__open$local) {
        $event.preventDefault();
        this.commitActive();
      }
    } else if (key === 'Escape') {
      if (__open$local) {
        $event.preventDefault();
        this.close();
        this.focusControl();
      }
    } else if (key === ' ' || key === 'Spacebar') {
      // Space toggles / commits in a select-only host (a button trigger). A
      // filter-input host types the literal space into its <input> and does NOT
      // route Space through this reducer, so this branch is select-only by use.
      $event.preventDefault();
      if (!__open$local) this.open();else this.commitActive();
    } else if (key === 'Tab') {
      if (__open$local) this.close();
    } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
      this.onTypeahead(key);
    }
  };
  // Combobox input handler: keep the popup open while typing, reset the active
  // highlight to the first match, and surface the query for remote filtering.
  // Pointer hover sets the virtual highlight (matches native <select> feel).
  onOptionPointerMove = (index: any) => {
    if (this.activeIndex() !== index) this.activeIndex.set(index);
  };
  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
  // in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
  // compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
  //
  // HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
  // by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
  //   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
  //                             returns its pre-pagination row model, listbox/combobox return the
  //                             filtered options). This partial MUST NOT reach into the host data engine
  //                             directly — rows arrive ONLY through windowSource().
  //   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
  //   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
  //   - gridScrollEl              — the scroll-container element handle.
  //   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
  //   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
  //   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
  //   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
  //                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
  //                             listbox passes nothing. Routing pinning through this host hook (NOT
  //                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
  //   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
  //                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
  //                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
  //                             convention exactly as they already call `pinnedEditIndex()`.
  //   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
  //                             host until it defines the real column-axis mechanism (87-04+).
  //   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
  //                             no default.
  //   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
  //                             from table-core's `getSize()` under D-06. REQUIRED, no default.
  //   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
  //                             DataTable host unions pinned + active-cell + editing column indices into
  //                             the column-window slice; listbox/combobox pass an empty array (host-
  //                             provided, defaulting to `[]`).
  //   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
  //                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
  //   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
  //                             data-table's real body reads `$props.autoMeasure === true`; listbox/
  //                             combobox/command-palette return `false` so the accumulator branch
  //                             estimateRowSize() gates on is dead code for them (D-20).
  //   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
  //                             assigned to refineRowEstimate() (below) by the host. The DataTable
  //                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
  //                             measureElement sweep so the fold + hysteresis re-feed run on every
  //                             window commit. Routed through a mutable `let` rather than a direct
  //                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
  //                             calling a bare-specifier-partial CONST is the exact forward-reference
  //                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
  //                             documents for columnVirtualizerOptions() (inlineScriptPartials()
  //                             groups the relative partial BEFORE the bare-specifier partial in the
  //                             merged per-target output, regardless of source import order). A
  //                             mutable `let` hoists to `useRef` on React and is excluded from a
  //                             useCallback's dependency array, sidestepping the hazard entirely — the
  //                             SAME mechanism `refreshRowModel` already relies on.
  //
  // AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
  // LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
  // note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
  // (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
  // `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
  // `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
  // (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
  // (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
  // (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
  // (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
  // axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
  // sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
  // Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
  // (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
  // hardcoded constant, separate from the row axis's `overscan: 8` below).
  //
  // isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
  // immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
  // signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
  // mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.
  // getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
  // React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
  // id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
  virtualItemKey = (i: any) => {
    const src = this.windowSource();
    return src && src[i] ? src[i].id : undefined;
  };
  // COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
  // row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
  // cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
  // overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
  // horizontal Virtualizer instance (see the AXIS MECHANISM note above).
  // ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
  // mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
  // hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
  // `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
  // precedent), so two DataTable instances on one page never share an accumulator
  // (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
  // row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
  // virtual-core (the hysteresis comparison baseline). ══
  measuredRowTotal = 0;
  measuredRowCount = 0;
  // ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
  // write behind a SINGLE microtask-deferred increment, regardless of how many callers request
  // one within the same synchronous JS task. ══
  //
  // ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
  // confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
  // (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
  // `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
  // TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
  // framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
  // (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
  // ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
  // synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
  // measured rows from the flat seed to their real heights) can fire onChange, and therefore an
  // UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
  // refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
  // folded those same measurements into the running mean or re-fed the converged estimate into
  // virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
  // getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
  // resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
  // reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
  // effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
  // resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
  // refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
  // reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
  // synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
  // correct by construction. Solid does not auto-batch a signal write made from outside a
  // Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
  // externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
  // virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
  // getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
  // the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
  // recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
  // further row happens to differ from its cache — so the DOM stays frozen on that stale,
  // pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
  // converged correctly (T-87-07's own confirmed finding).
  //
  // FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
  // refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
  // idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
  // EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
  // fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
  // batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
  // column widths come from table-core's getSize() oracle, never measured from the DOM), so
  // columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
  windowVerBumpPending = false;
  bumpWindowVer = (): void => {
    if (this.windowVerBumpPending) return;
    this.windowVerBumpPending = true;
    const flush = () => {
      this.windowVerBumpPending = false;
      this.windowVer.set(this.windowVer() + 1);
    };
    // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
    // (virtualization.rzts) for environments where queueMicrotask is unavailable.
    if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
  };
  // ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
  // virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
  // re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
  // own measureElement/resizeItem idempotence (see refineRowEstimate() below).
  // estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
  // FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
  // verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
  // regardless of autoMeasure) still returns the seed — the very first render has nothing
  // measured yet either way (D-15).
  estimateRowSize = (i: number): number => {
    const __estimateRowHeight = this.estimateRowHeight();
    if (!this.autoMeasureOn()) return __estimateRowHeight;
    if (this.measuredRowCount === 0) return __estimateRowHeight;
    return Math.round(this.measuredRowTotal / this.measuredRowCount);
  };
  // foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
  // accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
  // `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
  // source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
  // Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
  // on virtual-core's generic option inference. onChange's windowVer write is routed through
  // bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
  // this onChange MANY times in a single synchronous sweep (once per row whose real measured
  // size differs from its cache, e.g. every never-before-measured row in the FIRST window),
  // and coalescing those into one microtask-deferred write is what keeps every target's render
  // landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
  // Solid-specific rendering gap this closes). The React emitter still lowers the underlying
  // `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
  // microtask, exactly as it was correct from a mount closure before.
  virtualizerOptions = (): any => ({
    count: this.windowSource().length,
    getScrollElement: () => this.gridScrollEl,
    estimateSize: (i: any) => this.estimateRowSize(i),
    observeElementRect,
    observeElementOffset,
    scrollToFn: elementScroll,
    measureElement,
    overscan: 8,
    getItemKey: this.virtualItemKey,
    onChange: () => {
      this.bumpWindowVer();
      // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
      // virtual-core only observe()s a node you explicitly hand to measureElement (it does
      // NOT auto-discover rendered rows — measureElement is the SOLE caller of
      // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
      // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
      // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
      // so the new <tr> set is in the DOM before we measure. Safe from an infinite
      // measure→onChange→measure loop: measureElement is idempotent on an already-observed
      // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
      // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
      // re-measure is a no-op.
      this.scheduleRemeasure();
    }
  });
  // pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
  // shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
  // two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
  // no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
  // `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
  // guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
  // thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
  // gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
  // Typing-only: the runtime value (a measurement or null) is unchanged.
  pinMeasurement = (pin: number): {
    start: number;
    size: number;
    index: number;
    end: number;
  } | null => this.pinnedMeasurement(pin);
  // windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
  // { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
  // $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
  // full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
  // `rows` binding → TS2448 self-shadow, line ~1149 lesson).
  windowedRows = () => {
    const __rows = this.rows();
    // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
    // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
    // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
    // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
    // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
    // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
    // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
    // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
    // first re-run that picks up the now-non-null virtualizer.
    // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
    // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
    void this.windowVer();
    void this.editVer();
    if (!this.virtualizer) {
      // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
      // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
      // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
      // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
      // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
      // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
      // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
      // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
      // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
      // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
      // the first onChange after _didMount.
      if (!this.rowsWindowed()) {
        const rowList = __rows || [];
        return rowList.map((r: any, i: any) => ({
          vi: {
            index: i
          },
          row: r
        }));
      }
      return [];
    }
    const items = this.virtualizer.getVirtualItems();
    const rowList = __rows || [];
    // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
    // shrink window where the virtualizer count is stale relative to $data.rows on the async
    // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
    // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
    const out = items.map((vi: any) => ({
      vi,
      row: rowList[vi.index]
    })).filter((wr: any) => wr.row);
    // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
    // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
    // into another full-model row), LEADING the slice when it sits above the window and TRAILING
    // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
    // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
    // into the real windowing.
    const pin = this.pinnedEditIndex();
    if (pin >= 0 && rowList[pin]) {
      let inWindow = false;
      for (let i = 0; i < items.length; i++) {
        if (items[i].index === pin) {
          inWindow = true;
          break;
        }
      }
      if (!inWindow) {
        const pm = this.pinMeasurement(pin);
        const firstStart = items.length ? items[0].start : 0;
        const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
        const pinnedEntry = {
          vi: pm != null ? pm : {
            index: pin
          },
          row: rowList[pin],
          pinned: true
        };
        if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
      }
    }
    return out;
  };
  // Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
  // the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
  // (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
  padTop = () => {
    // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
    // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
    // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
    void this.windowVer();
    void this.editVer();
    if (!this.rowsWindowed() || !this.virtualizer) return 0;
    const items = this.virtualizer.getVirtualItems();
    let pad = items.length ? items[0].start : 0;
    // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
    // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
    // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
    const pin = this.pinnedEditIndex();
    if (pin >= 0) {
      const pm = this.pinMeasurement(pin);
      const inWindow = this.pmIndexInWindow(items, pin);
      if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
    }
    return pad < 0 ? 0 : pad;
  };
  padBottom = () => {
    // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
    // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
    // on pin/unpin.
    void this.windowVer();
    void this.editVer();
    if (!this.rowsWindowed() || !this.virtualizer) return 0;
    const items = this.virtualizer.getVirtualItems();
    if (!items.length) return 0;
    let pad = this.virtualizer.getTotalSize() - items[items.length - 1].end;
    // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
    // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
    const pin = this.pinnedEditIndex();
    if (pin >= 0) {
      const pm = this.pinMeasurement(pin);
      const inWindow = this.pmIndexInWindow(items, pin);
      // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
      // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
      // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
      // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
      // the offset comparison only if the measurement lacks an index (defensive).
      const lastItemIdx = items[items.length - 1].index;
      const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
      if (pm && !inWindow && below) {
        // below the window → it trailed the slice; subtract its height from the trailing spacer.
        if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
      }
    }
    return pad < 0 ? 0 : pad;
  };
  // pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
  pmIndexInWindow = (items: any, idx: any) => {
    for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
    return false;
  };
  // rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
  // window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
  rowIsOutsideWindow = (r: any) => {
    if (!this.rowsWindowed() || !this.virtualizer) return false;
    const items = this.virtualizer.getVirtualItems();
    for (const it of items as any) if (it.index === r) return false;
    return true;
  };
  // ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
  // rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
  // a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
  // returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
  // cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══
  // windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
  virtualizer: any = null;
  virtualizerCleanup: any = null;
  gridScrollEl: any = null;
  remeasurePending = false;
  // Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
  // the option count at that moment, and the last scrollTop already accounted for.
  scrollEndPinned = false;
  scrollEndPinnedCount = -1;
  scrollEndPinnedTop = -1;
  // windowSource(): the windowing.rzts host-contract row source — the FILTERED option
  // set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
  // (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
  // wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
  // — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
  // every framework :key (and every virtual-core measurement key) to `undefined`, which
  // recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
  // row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
  // value, `_opt` the original option (read via wr.row._opt in the windowed template),
  // `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
  // the same wrapped row the count windows over.
  //
  // $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
  // FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
  // would never hit while filtering). The key covers everything the map reads:
  // options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
  // resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
  // Same reference-stability contract the sibling Combobox's filteredOptions carries —
  // virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
  // unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
  windowSourceCache = {
    keys: null as any[] | null,
    val: null as any
  };
  windowSource = () => {
    const __rozieMemoKey = [this.options(), this.query(), this.optionValue(), this.optionLabel()];
    const __rozieMemoPrev = this.windowSourceCache.keys;
    if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
      return this.windowSourceCache.val;
    }
    const __rozieMemoVal = this.visibleOptions().map((o: any, i: any) => ({
      id: this.valueOf$local(o),
      _opt: o,
      _i: i
    }));
    this.windowSourceCache.keys = __rozieMemoKey;
    this.windowSourceCache.val = __rozieMemoVal;
    return __rozieMemoVal;
  };
  // D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
  // A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
  // pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
  // reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
  // to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
  pinnedEditIndex = () => -1;
  pinnedMeasurement = (pin: any) => null;
  // D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
  // today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
  // windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
  // `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
  // pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
  // in this plan calls these through a type-narrowing wrapper.
  //
  // GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
  // `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
  // alongside this were REMOVED here — they were dead code shipped on a mistaken premise
  // about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
  // virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
  // from windowing.rzts; none of those functions' bodies reference the column-axis symbols
  // (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
  // `colIsOutsideWindow()` do, and Listbox never imports any of those), so
  // `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
  // 87-16-SUMMARY.md for the verification trail.
  rowsWindowed = () => !!this.virtual();
  // autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
  // gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
  // estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
  // column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
  // — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
  // `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
  autoMeasureOn = () => false;
  // Keep $data.rows === windowSource() so the windowing math indexes the live option set.
  syncRows = () => {
    this.rows.set(this.windowSource());
  };
  // SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
  // variable-height list at the end while the options in view measure taller than their
  // estimate. The view is judged on the DOM, and only at a move the USER made — a move is
  // virtual-core's own when it still holds an unreconciled scroll adjustment
  // (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
  // computed from its last-observed (stale) offset, so it pulls the view back up from the end
  // and must neither clear the pin nor be mistaken for the user leaving the end. That position
  // is remembered so the scroll event that later reports it is not read as a user move either.
  // (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
  // with every option measured in the ResizeObserver batch while its offset stays at the stale
  // value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
  recordScrollEnd = () => {
    if (!this.virtualizer || !this.gridScrollEl || this.virtualizer.scrollState) return;
    const top = this.gridScrollEl.scrollTop;
    if (top === this.scrollEndPinnedTop) return;
    this.scrollEndPinnedTop = top;
    if (this.virtualizer.scrollAdjustments !== 0) return;
    // Only a list that actually overflows has an end to hold: while the window has not painted
    // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
    // recorded then would jump the freshly opened list to the bottom.
    const sh = this.gridScrollEl.scrollHeight;
    const ch = this.gridScrollEl.clientHeight;
    this.scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
    this.scrollEndPinnedCount = this.windowSource().length;
  };
  // Re-apply the pin after the framework has committed the window (called from the rAF pass):
  // the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
  // flight, and not when the option count changed since the user reached the end (a new filter
  // or appended options must not be auto-followed).
  keepScrollEnd = () => {
    if (!this.scrollEndPinned || !this.virtualizer || !this.gridScrollEl || this.virtualizer.scrollState) return;
    if (this.windowSource().length !== this.scrollEndPinnedCount) return;
    const maxTop = this.gridScrollEl.scrollHeight - this.gridScrollEl.clientHeight;
    if (maxTop - this.gridScrollEl.scrollTop > 1) {
      this.gridScrollEl.scrollTop = maxTop;
      this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
    }
  };
  // Defer remeasureWindow() until AFTER the framework commits the recycled window
  // (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
  // behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
  // per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
  // SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
  // budget), the rAF catches React's async commit. measureElement is idempotent on an
  // already-observed node, so running both is cheap and loop-free.
  scheduleRemeasure = () => {
    this.recordScrollEnd();
    if (this.remeasurePending) return;
    this.remeasurePending = true;
    let ranMicro = false;
    const microPass = () => {
      this.remeasureWindow();
    };
    // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
    // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
    // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
    // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
    // committed options cover the virtualizer's window, bounded (the data-table host twin).
    let rafAttempts = 0;
    const rafPass = () => {
      const covered = this.remeasureWindow();
      rafAttempts = rafAttempts + 1;
      if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
        requestAnimationFrame(rafPass);
        return;
      }
      this.keepScrollEnd();
      this.remeasurePending = false;
    };
    if (typeof queueMicrotask !== 'undefined') {
      ranMicro = true;
      queueMicrotask(microPass);
    }
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) this.remeasurePending = false;else setTimeout(rafPass, 0);
  };
  // measureElement sweep: hand every rendered windowed option to the virtualizer so its
  // true (variable) height is observed (virtual-core measures ONLY nodes passed to
  // measureElement, keyed by the data-index attribute). Bails during a programmatic
  // scroll (scrollToIndex) so a measure can't starve the scroll target.
  remeasureWindow = () => {
    if (!this.virtualizer || !this.gridScrollEl) return true;
    if (this.virtualizer.scrollState) return true;
    const els = this.gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]');
    const rendered = new Set();
    for (const el of els as any) {
      this.virtualizer.measureElement(el);
      rendered.add(el.getAttribute('data-index'));
    }
    // N-05: false while the framework has not yet committed the recycled window.
    const items = this.virtualizer.getVirtualItems();
    for (let i = 0; i < items.length; i++) {
      if (!rendered.has(String(items[i].index))) return false;
    }
    return true;
  };
  // ---- focus / scroll helpers (post-mount $refs only) --------------------
  // Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
  // only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
  // verb would override the inherited HTMLElement.focus method on the Lit element.
  focusControl = () => {
    this.triggerEl()?.nativeElement?.focus();
  };
  // Keep the active option visible inside the scrolling listbox. Reads $refs in
  // a post-mount callback only (never eagerly — ROZ123). When windowing, route through
  // the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
  // scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
  scrollActiveIntoView = () => {
    const __activeIndex = this.activeIndex();
    if (__activeIndex < 0) return;
    if (this.virtual() && this.virtualizer) {
      // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
      // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
      // leave it just-unrendered for a frame on the fine-grained targets (Solid).
      this.virtualizer.scrollToIndex(__activeIndex, {
        align: 'center'
      });
      this.scheduleRemeasure();
      return;
    }
    if (!this.listEl()?.nativeElement) return;
    const el = this.listEl()!.nativeElement.querySelector('#' + CSS.escape(this.optionId(__activeIndex)));
    el?.scrollIntoView({
      block: 'nearest'
    });
  };
  // ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
  // kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
  // re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
  // (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
  // re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
  // asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
  // (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
  // scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
  // (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
  // paints (or attempts run out) — idempotent + loop-free.
  kickWindow = (attempts: any) => {
    if (!this.virtualizer) return;
    this.gridScrollEl = this.__rozieRoot()?.nativeElement ? this.__rozieRoot()!.nativeElement.querySelector('.rozie-listbox-list') : this.gridScrollEl;
    // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
    // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
    // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
    // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
    if (this.windowSource().length > 0) {
      this.syncRows();
      this.virtualizer.setOptions(this.virtualizerOptions());
    }
    this.virtualizer._willUpdate();
    this.windowVer.set(this.windowVer() + 1);
    this.remeasureWindow();
    if (this.windowedRows().length === 0 && attempts > 0) {
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => this.kickWindow(attempts - 1));else setTimeout(() => this.kickWindow(attempts - 1), 16);
    }
  };
  // nextAutoId(): a page-wide counter shared by every Rozie component instance. It
  // lives on globalThis (read through Reflect, which type-checks in the plain-JS and
  // the TS script alike) so separately bundled copies of a leaf never hand out the
  // same id. The same four lines live in Combobox, Listbox and Popover.
  nextAutoId = () => {
    const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
    Reflect.set(globalThis, '__rozieAutoId', n);
    return n;
  };
  // idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
  // pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
  idRoot = () => this.id() || this.autoId() || 'rozie-listbox';

  private __rozieCvaOnChange: (v: unknown) => void = () => {};
  private __rozieCvaOnTouchedFn: () => void = () => {};
  protected __rozieCvaDisabled = signal(false);

  writeValue(v: unknown | null): void {
    this.value.set(v ?? null);
  }
  registerOnChange(fn: (v: unknown) => void): void {
    this.__rozieCvaOnChange = fn;
  }
  registerOnTouched(fn: () => void): void {
    this.__rozieCvaOnTouchedFn = fn;
  }
  setDisabledState(isDisabled: boolean): void {
    this.__rozieCvaDisabled.set(isDisabled);
  }
  __rozieCvaOnTouched(): void {
    this.__rozieCvaOnTouchedFn();
  }

  static ngTemplateContextGuard(
    _dir: Listbox,
    _ctx: unknown,
  ): _ctx is SelectedCtx | OptionCtx | EmptyCtx {
    return true;
  }

  private __rozieDestroyRef = inject(DestroyRef);

  private rozieSpread_0 = viewChild<ElementRef>('rozieSpread_0');

  private __rozieApplyAttrs = createRozieAttrApplier(inject(Renderer2));

  private __rozieGetHostAttrs = createRozieHostAttrsReader(inject(ElementRef));

  private __rozieSpread_0_effect = afterRenderEffect(() => {
    const el = this.rozieSpread_0()?.nativeElement;
    if (!el) return;
    this.__rozieApplyAttrs(el, this.__rozieGetHostAttrs());
  });

  private rozieListenersTarget_1 = viewChild<ElementRef>('rozieListenersTarget_1');

  private __rozieListenersRenderer = inject(Renderer2);

  private __rozieListenersDisposers_1: Array<() => void> = [];

  private __rozieListenersDestroyRegistered_1 = false;

  private __rozieListenersEffect_1 = effect(() => {
    const el = this.rozieListenersTarget_1()?.nativeElement;
    if (!el) return;
    for (const off of this.__rozieListenersDisposers_1) off();
    this.__rozieListenersDisposers_1 = [];
    const obj: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(obj)) {
      if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
      if (typeof v !== 'function') continue;
      const norm = k.startsWith('on') ? k.slice(2).toLowerCase() : k;
      const dispose = this.__rozieListenersRenderer.listen(el, norm, v as EventListener);
      this.__rozieListenersDisposers_1.push(dispose);
    }
    if (!this.__rozieListenersDestroyRegistered_1) {
      this.__rozieListenersDestroyRegistered_1 = true;
      this.__rozieDestroyRef.onDestroy(() => {
        for (const off of this.__rozieListenersDisposers_1) off();
        this.__rozieListenersDisposers_1 = [];
      });
    }
  });

  protected get __style() {
      const __maxHeight = this.maxHeight();
      return (this.open$local() ? '' : 'display:none;') + (__maxHeight ? 'height:' + __maxHeight + ';max-height:' + __maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + __maxHeight : 'overflow-y:auto');
    }

  rozieDisplay(v: unknown): string { return __rozieDisplay(v); }

  rozieAttr(v: unknown): string | null { return __rozieAttr(v); }
}

export default Listbox;
tsx
import type { JSX } from 'solid-js';
import { For, Show, createEffect, createMemo, createSignal, mergeProps, on, onCleanup, onMount, splitProps, untrack } from 'solid-js';
import { Key } from '@solid-primitives/keyed';
import { __rozieInjectStyle, createControllableSignal, createOutsideClick, parseInlineStyle, rozieAttr, rozieClass, rozieDisplay } from '@rozie/runtime-solid';
// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';

// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.

__rozieInjectStyle('Listbox-b576227a', `.rozie-listbox[data-rozie-s-b576227a] {
  position: relative;
  display: inline-block;
  min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
  font: var(--rozie-listbox-font, inherit);
}
.rozie-listbox-control[data-rozie-s-b576227a] { display: block; }
.rozie-listbox-input[data-rozie-s-b576227a],
.rozie-listbox-trigger[data-rozie-s-b576227a] {
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
  padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
  font: inherit;
  text-align: left;
  background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
  border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
  cursor: pointer;
}
.rozie-listbox-input[data-rozie-s-b576227a] { cursor: text; }
.rozie-listbox-input[data-rozie-s-b576227a]:focus-visible,
.rozie-listbox-input[data-rozie-s-b576227a]:focus,
.rozie-listbox-trigger[data-rozie-s-b576227a]:focus-visible,
.rozie-listbox-trigger[data-rozie-s-b576227a]:focus {
  outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
  outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
}
.rozie-listbox-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
.rozie-listbox-placeholder[data-rozie-s-b576227a] { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
.rozie-listbox-arrow[data-rozie-s-b576227a] {
  font-size: 0.75em;
  color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
  opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
}
.rozie-listbox-list[data-rozie-s-b576227a] {
  position: absolute;
  z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
  top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
  max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
  overflow-y: auto;
  list-style: none;
  background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
  border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
  box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
}
.rozie-listbox-inline[data-rozie-s-b576227a] {
  display: block;
  width: 100%;
}
.rozie-listbox-inline[data-rozie-s-b576227a] .rozie-listbox-list[data-rozie-s-b576227a] {
  position: static;
  margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.rozie-listbox-option[data-rozie-s-b576227a] {
  padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
  border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
  color: var(--rozie-listbox-option-fg, inherit);
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
}
.rozie-listbox-option.is-active[data-rozie-s-b576227a] {
  background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
  color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
}
.rozie-listbox-option.is-selected[data-rozie-s-b576227a] {
  background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
  color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
  font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
}
.rozie-listbox-option.is-selected[data-rozie-s-b576227a]::after {
  content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
  color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
}
.rozie-listbox-option.is-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
.rozie-listbox-empty[data-rozie-s-b576227a] { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
.rozie-listbox-spacer[data-rozie-s-b576227a] { margin: 0; padding: 0; border: 0; flex: none; }
.rozie-listbox-list--virtual[data-rozie-s-b576227a] { overflow-anchor: none; }`);

interface SelectedSlotCtx { selected: any; value: any; }

interface OptionSlotCtx { option: any; index: any; active: any; selected: any; disabled: any; }

interface EmptySlotCtx { query: any; }

interface ListboxProps extends Omit<import('solid-js').ComponentProps<'div'>, 'options' | 'value' | 'defaultValue' | 'onValueChange' | 'multiple' | 'inline' | 'disabled' | 'placeholder' | 'closeOnSelect' | 'optionLabel' | 'optionValue' | 'optionDisabled' | 'id' | 'ariaLabel' | 'virtual' | 'estimateRowHeight' | 'maxHeight' | 'onOpenChange' | 'onChange' | 'selectedSlot' | 'optionSlot' | 'emptySlot' | 'slots' | 'ref' | 'children' | 'innerHTML' | 'innerText' | 'textContent'> {
  /**
   * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
   */
  options?: any[];
  /**
   * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
   * @example
   * <Listbox value={fruit()} onValueChange={setFruit} options={fruits} />
   */
  value?: (unknown) | null;
  defaultValue?: (unknown) | null;
  onValueChange?: (value: (unknown) | null) => void;
  /**
   * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
   */
  multiple?: boolean;
  /**
   * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
   */
  inline?: boolean;
  /**
   * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
   */
  disabled?: boolean;
  /**
   * Placeholder text shown in the empty control.
   */
  placeholder?: string;
  /**
   * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
   */
  closeOnSelect?: boolean;
  /**
   * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
   */
  optionLabel?: ((...args: any[]) => any) | null;
  /**
   * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
   */
  optionValue?: ((...args: any[]) => any) | null;
  /**
   * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
   */
  optionDisabled?: ((...args: any[]) => any) | null;
  /**
   * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
   */
  id?: string;
  /**
   * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
   */
  ariaLabel?: (string) | null;
  /**
   * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
   */
  virtual?: boolean;
  /**
   * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
   */
  estimateRowHeight?: number;
  /**
   * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
   */
  maxHeight?: string;
  onOpenChange?: (...args: any[]) => void;
  onChange?: (...args: any[]) => void;
  selectedSlot?: (ctx: SelectedSlotCtx) => JSX.Element;
  optionSlot?: (ctx: OptionSlotCtx) => JSX.Element;
  emptySlot?: (ctx: EmptySlotCtx) => JSX.Element;
  slots?: Record<string, (ctx: any) => JSX.Element>;
  ref?: (h: ListboxHandle) => void;
}

export interface ListboxHandle {
  open: (...args: any[]) => any;
  close: (...args: any[]) => any;
  toggle: (...args: any[]) => any;
  clear: (...args: any[]) => any;
  focusControl: (...args: any[]) => any;
}

export default function Listbox(_props: ListboxProps): JSX.Element {
  const _merged = mergeProps({ options: (() => [])() as any[], multiple: false, inline: false, disabled: false, placeholder: '', closeOnSelect: true, optionLabel: null, optionValue: null, optionDisabled: null, id: '', ariaLabel: null, virtual: false, estimateRowHeight: 36, maxHeight: '' }, _props);
  const [local, attrs] = splitProps(_merged, ['options', 'value', 'multiple', 'inline', 'disabled', 'placeholder', 'closeOnSelect', 'optionLabel', 'optionValue', 'optionDisabled', 'id', 'ariaLabel', 'virtual', 'estimateRowHeight', 'maxHeight', 'ref', 'onOpenChange', 'onChange']);
  onMount(() => { local.ref?.({ open, close, toggle, clear, focusControl }); });

  const [value, setValue] = createControllableSignal<unknown>(_props as unknown as Record<string, unknown>, 'value', null);
  const [autoId, setAutoId] = createSignal('');
  const [open$local, setOpen$local] = createSignal(false);
  const [activeIndex, setActiveIndex] = createSignal(-1);
  const [query, setQuery] = createSignal('');
  const [rows, setRows] = createSignal<any[]>([]);
  const [windowVer, setWindowVer] = createSignal(0);
  const [editVer, setEditVer] = createSignal(0);
  const selectedLabel = createMemo(() => {
    const cur = value();
    if (local.multiple) {
      // Read the model value into a local before narrowing: `$props.value` lowers
      // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
      // separate calls — narrowing one stable local works on every target.
      const arr = Array.isArray(cur) ? cur : [];
      if (arr.length === 0) return '';
      return local.options.filter((o: any) => arr.includes(valueOf(o))).map(labelOf).join(', ');
    }
    const match = local.options.find((o: any) => valueOf(o) === cur);
    return match === undefined ? '' : labelOf(match);
  });
  const activeDescendant = createMemo(() => {
    if (!open$local() || activeIndex() < 0) return null;
    return optionId(activeIndex());
  });
  onMount(() => {
    if (!local.id) setAutoId('rozie-listbox-' + nextAutoId());
    syncRows();
    if (local.virtual) {
      // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
      // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
      // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
      // Solid/Lit, which leaves the virtualizer with no scroll element.
      gridScrollEl = __rozieRootRef! ? __rozieRootRef!.querySelector('.rozie-listbox-list') : null;
      virtualizer = new Virtualizer(virtualizerOptions());
      virtualizerCleanup = virtualizer._didMount();
      setWindowVer(windowVer() + 1);
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(8));else setTimeout(() => kickWindow(8), 0);
    }
  });
  onCleanup(() => {
    if (typeTimer !== null) clearTimeout(typeTimer);
    // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
    if (virtualizerCleanup) virtualizerCleanup();
  });
  createEffect(on(() => (() => (local.options ? local.options.length : 0) + '|' + query())(), (v) => untrack(() => (() => {
    syncRows();
    if (local.virtual && virtualizer) {
      gridScrollEl = __rozieRootRef! ? __rozieRootRef!.querySelector('.rozie-listbox-list') : gridScrollEl;
      virtualizer.setOptions(virtualizerOptions());
      virtualizer._willUpdate();
      setWindowVer(windowVer() + 1);
      scheduleRemeasure();
    }
  })()), { defer: true }));
  let controlElRef: HTMLElement | null = null;
  let triggerElRef: HTMLElement | null = null;
  let listElRef: HTMLElement | null = null;
  let __rozieRootRef: HTMLElement | null = null;

  // Type-ahead buffer for the select-only listbox trigger. Module-scope
  // `let`s reassigned from handlers → the React emitter hoists them to `useRef`
  // so they persist across renders (the setup-once guarantee); no-op elsewhere.
  // They STAY in this host (not the shared spine) per the A==B rule: reassigned
  // module-`let`s + sigils live in the host; the partial only closes over them.
  let typeBuffer = '';
  let typeTimer: any = null;

  // ---- shared list spine (P2: @rozie-ui/headless-core/listCore.rzts) ------
  // The option resolvers, client filter, enabled-index navigation, the keyboard
  // reducer, type-ahead, single+multi selection, open/close state, and
  // activeDescendant derivation now live in the shared, focus-/input-mode
  // parameterized list spine. It is a compile-time `.rzts` script-partial: it
  // dissolves into this leaf via inlineScriptPartials() before IR lowering (zero
  // runtime dep). Listbox consumes it in focus-model `activedescendant` +
  // input-mode `select-only` + multi + type-ahead. The spine closes over this
  // host's pieces by convention: the reassigned module-`let`s typeBuffer/typeTimer
  // (above) and the impure ref fns focusControl/scrollActiveIntoView (below).
  // ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
  // Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
  // partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
  // navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
  // selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
  // script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
  // IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
  //
  // ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
  // The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
  // windowing.rzts uses) along two axes:
  //   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
  //     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
  //     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
  //     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
  //     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
  //     activedescendant model.
  //   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
  //     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
  //     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
  //     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
  //     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
  //     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
  //
  // ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
  //   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
  //     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
  //     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
  //     over them (in `onTypeahead`).
  //   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
  //     generates in $onMount); `optionId` below derives every option id from it.
  //   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
  //     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
  //     HOST functions; this partial only closes over them (it reads NO refs itself).
  //   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
  //     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
  //     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
  //     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
  //     `$data.query`), NOT a discriminant prop.

  // ---- option resolvers --------------------------------------------------
  function labelOf(opt: any) {
    if (local.optionLabel !== null) return local.optionLabel(opt);
    if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
    return String(opt);
  }
  function valueOf(opt: any) {
    if (local.optionValue !== null) return local.optionValue(opt);
    if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
    return opt;
  }
  function disabledOf(opt: any) {
    if (local.optionDisabled !== null) return !!local.optionDisabled(opt);
    if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
    return false;
  }
  // `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
  // per-instance id) so the option ids follow the host's auto-id fallback.
  function optionId(index: any) {
    return idRoot() + '-opt-' + index;
  }

  // ---- derived state -----------------------------------------------------
  // The visible option list: identity in select-only / non-filtering mode,
  // a case-insensitive substring filter when a combobox query is present.
  // A plain function (not `$computed`) so it reads uniformly across all six
  // targets — a `$computed` is a value on React but an accessor on Solid, so
  // aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
  // plain function is identical everywhere.
  function visibleOptions() {
    const q = (query() || '').trim().toLowerCase();
    if (q === '') return local.options;
    return local.options.filter((opt: any) => labelOf(opt).toLowerCase().includes(q));
  }

  // The label shown in the (select-only) trigger when closed. A real `$computed`
  // — read bare in the template, never aliased in script, so the per-target
  // accessor form stays uniform.

  // Is a given option currently selected? Multi compares array membership.
  function isSelected(opt: any) {
    const v = valueOf(opt);
    const cur = value();
    if (local.multiple) return Array.isArray(cur) && cur.includes(v);
    return cur === v;
  }

  // First enabled visible index, preferring the currently-selected option.
  function resolveInitialActive() {
    const opts = visibleOptions();
    const sel = opts.findIndex((o: any) => isSelected(o) && !disabledOf(o));
    if (sel !== -1) return sel;
    return opts.findIndex((o: any) => !disabledOf(o));
  }

  // ---- open / close ------------------------------------------------------
  // Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
  // $emit's directly — no longer funneled through a single wrapper. The
  // former "route every emit through ONE wrapper fn" workaround guarded
  // against a React duplicate `const {onOpenChange}=props` per emit-site
  // (TS2451); verified against the current emitter (target-react
  // `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
  // hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
  // target into exactly one destructure, and a non-escaping function (e.g.
  // `open`, reachable here only via `$expose`) never destructures at all — so
  // no combination of these three functions can produce the duplicate-const
  // shape. See project_next_port_listbox / project_emitter_hardening_backlog.
  function open() {
    if (local.disabled) return;
    if (open$local()) return;
    setOpen$local(true);
    setActiveIndex(resolveInitialActive());
    _props.onOpenChange?.({
      open: true
    });
  }
  function close() {
    if (!open$local()) return;
    setOpen$local(false);
    setActiveIndex(-1);
    _props.onOpenChange?.({
      open: false
    });
  }
  function toggle() {
    if (open$local()) close();else open();
  }

  // ---- selection ---------------------------------------------------------
  function select(opt: any) {
    if (disabledOf(opt)) return;
    const v = valueOf(opt);
    if (local.multiple) {
      const cur = value();
      const arr = Array.isArray(cur) ? cur : [];
      // Fresh array on every commit — in-place mutation is dropped by the
      // React/Solid/Lit/Angular change detectors.
      const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
      setValue(next);
      _props.onChange?.({
        value: next,
        option: opt
      });
    } else {
      setValue(v);
      _props.onChange?.({
        value: v,
        option: opt
      });
      if (local.closeOnSelect) {
        close();
        focusControl();
      }
    }
  }
  function clear() {
    const empty = local.multiple ? [] : null;
    setValue(empty);
    setQuery('');
    _props.onChange?.({
      value: empty,
      option: null
    });
  }

  // ---- keyboard navigation over the VISIBLE list -------------------------
  function nextEnabled(from: any, dir: any) {
    const opts = visibleOptions();
    if (opts.length === 0) return -1;
    let i = from;
    for (let step = 0; step < opts.length; step++) {
      i += dir;
      if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
      if (!disabledOf(opts[i])) return i;
    }
    return from;
  }
  function move(dir: any) {
    if (!open$local()) {
      open();
      return;
    }
    const start = activeIndex() < 0 ? dir > 0 ? -1 : 0 : activeIndex();
    setActiveIndex(nextEnabled(start, dir));
    scrollActiveIntoView();
  }
  function moveEdge(toEnd: any) {
    if (!open$local()) open();
    setActiveIndex(toEnd ? nextEnabled(-1, -1) : nextEnabled(-1, 1));
    scrollActiveIntoView();
  }
  function commitActive() {
    const opts = visibleOptions();
    if (activeIndex() >= 0 && activeIndex() < opts.length) select(opts[activeIndex()]);
  }

  // Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
  // first option whose label starts with the buffer.
  function onTypeahead(ch: any) {
    if (typeTimer !== null) clearTimeout(typeTimer);
    typeBuffer += ch.toLowerCase();
    typeTimer = setTimeout(() => {
      typeBuffer = '';
    }, 600);
    const opts = visibleOptions();
    const idx = opts.findIndex((o: any) => !disabledOf(o) && labelOf(o).toLowerCase().startsWith(typeBuffer));
    if (idx !== -1) {
      if (!open$local()) open();
      setActiveIndex(idx);
      scrollActiveIntoView();
    }
  }

  // Key handler shared by the trigger and the combobox input. The printable-
  // character branch is reached only in select-only mode (the combobox input
  // types through @input).
  function onControlKeyDown($event: any) {
    const key = $event.key;
    if (key === 'ArrowDown') {
      $event.preventDefault();
      move(1);
    } else if (key === 'ArrowUp') {
      $event.preventDefault();
      move(-1);
    } else if (key === 'Home') {
      $event.preventDefault();
      moveEdge(false);
    } else if (key === 'End') {
      $event.preventDefault();
      moveEdge(true);
    } else if (key === 'Enter') {
      if (open$local()) {
        $event.preventDefault();
        commitActive();
      }
    } else if (key === 'Escape') {
      if (open$local()) {
        $event.preventDefault();
        close();
        focusControl();
      }
    } else if (key === ' ' || key === 'Spacebar') {
      // Space toggles / commits in a select-only host (a button trigger). A
      // filter-input host types the literal space into its <input> and does NOT
      // route Space through this reducer, so this branch is select-only by use.
      $event.preventDefault();
      if (!open$local()) open();else commitActive();
    } else if (key === 'Tab') {
      if (open$local()) close();
    } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
      onTypeahead(key);
    }
  }

  // Combobox input handler: keep the popup open while typing, reset the active
  // highlight to the first match, and surface the query for remote filtering.

  // Pointer hover sets the virtual highlight (matches native <select> feel).
  function onOptionPointerMove(index: any) {
    if (activeIndex() !== index) setActiveIndex(index);
  }

  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
  // in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
  // compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
  //
  // HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
  // by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
  //   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
  //                             returns its pre-pagination row model, listbox/combobox return the
  //                             filtered options). This partial MUST NOT reach into the host data engine
  //                             directly — rows arrive ONLY through windowSource().
  //   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
  //   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
  //   - gridScrollEl              — the scroll-container element handle.
  //   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
  //   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
  //   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
  //   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
  //                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
  //                             listbox passes nothing. Routing pinning through this host hook (NOT
  //                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
  //   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
  //                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
  //                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
  //                             convention exactly as they already call `pinnedEditIndex()`.
  //   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
  //                             host until it defines the real column-axis mechanism (87-04+).
  //   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
  //                             no default.
  //   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
  //                             from table-core's `getSize()` under D-06. REQUIRED, no default.
  //   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
  //                             DataTable host unions pinned + active-cell + editing column indices into
  //                             the column-window slice; listbox/combobox pass an empty array (host-
  //                             provided, defaulting to `[]`).
  //   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
  //                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
  //   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
  //                             data-table's real body reads `$props.autoMeasure === true`; listbox/
  //                             combobox/command-palette return `false` so the accumulator branch
  //                             estimateRowSize() gates on is dead code for them (D-20).
  //   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
  //                             assigned to refineRowEstimate() (below) by the host. The DataTable
  //                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
  //                             measureElement sweep so the fold + hysteresis re-feed run on every
  //                             window commit. Routed through a mutable `let` rather than a direct
  //                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
  //                             calling a bare-specifier-partial CONST is the exact forward-reference
  //                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
  //                             documents for columnVirtualizerOptions() (inlineScriptPartials()
  //                             groups the relative partial BEFORE the bare-specifier partial in the
  //                             merged per-target output, regardless of source import order). A
  //                             mutable `let` hoists to `useRef` on React and is excluded from a
  //                             useCallback's dependency array, sidestepping the hazard entirely — the
  //                             SAME mechanism `refreshRowModel` already relies on.
  //
  // AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
  // LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
  // note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
  // (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
  // `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
  // `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
  // (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
  // (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
  // (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
  // (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
  // axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
  // sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
  // Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
  // (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
  // hardcoded constant, separate from the row axis's `overscan: 8` below).
  //
  // isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
  // immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
  // signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
  // mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.

  // getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
  // React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
  // id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
  function virtualItemKey(i: any) {
    const src = windowSource();
    return src && src[i] ? src[i].id : undefined;
  }

  // COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
  // row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
  // cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
  // overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
  // horizontal Virtualizer instance (see the AXIS MECHANISM note above).

  // ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
  // mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
  // hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
  // `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
  // precedent), so two DataTable instances on one page never share an accumulator
  // (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
  // row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
  // virtual-core (the hysteresis comparison baseline). ══
  let measuredRowTotal = 0;
  let measuredRowCount = 0;

  // ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
  // write behind a SINGLE microtask-deferred increment, regardless of how many callers request
  // one within the same synchronous JS task. ══
  //
  // ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
  // confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
  // (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
  // `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
  // TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
  // framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
  // (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
  // ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
  // synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
  // measured rows from the flat seed to their real heights) can fire onChange, and therefore an
  // UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
  // refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
  // folded those same measurements into the running mean or re-fed the converged estimate into
  // virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
  // getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
  // resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
  // reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
  // effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
  // resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
  // refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
  // reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
  // synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
  // correct by construction. Solid does not auto-batch a signal write made from outside a
  // Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
  // externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
  // virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
  // getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
  // the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
  // recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
  // further row happens to differ from its cache — so the DOM stays frozen on that stale,
  // pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
  // converged correctly (T-87-07's own confirmed finding).
  //
  // FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
  // refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
  // idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
  // EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
  // fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
  // batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
  // column widths come from table-core's getSize() oracle, never measured from the DOM), so
  // columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
  let windowVerBumpPending = false;
  function bumpWindowVer(): void {
    if (windowVerBumpPending) return;
    windowVerBumpPending = true;
    const flush = () => {
      windowVerBumpPending = false;
      setWindowVer(windowVer() + 1);
    };
    // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
    // (virtualization.rzts) for environments where queueMicrotask is unavailable.
    if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
  }

  // ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
  // virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
  // re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
  // own measureElement/resizeItem idempotence (see refineRowEstimate() below).

  // estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
  // FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
  // verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
  // regardless of autoMeasure) still returns the seed — the very first render has nothing
  // measured yet either way (D-15).
  function estimateRowSize(i: number): number {
    if (!autoMeasureOn()) return local.estimateRowHeight;
    if (measuredRowCount === 0) return local.estimateRowHeight;
    return Math.round(measuredRowTotal / measuredRowCount);
  }

  // foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
  // accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).

  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
  // `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
  // source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
  // Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
  // on virtual-core's generic option inference. onChange's windowVer write is routed through
  // bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
  // this onChange MANY times in a single synchronous sweep (once per row whose real measured
  // size differs from its cache, e.g. every never-before-measured row in the FIRST window),
  // and coalescing those into one microtask-deferred write is what keeps every target's render
  // landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
  // Solid-specific rendering gap this closes). The React emitter still lowers the underlying
  // `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
  // microtask, exactly as it was correct from a mount closure before.
  function virtualizerOptions(): any {
    return {
      count: windowSource().length,
      getScrollElement: () => gridScrollEl,
      estimateSize: (i: any) => estimateRowSize(i),
      observeElementRect,
      observeElementOffset,
      scrollToFn: elementScroll,
      measureElement,
      overscan: 8,
      getItemKey: virtualItemKey,
      onChange: () => {
        bumpWindowVer();
        // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
        // virtual-core only observe()s a node you explicitly hand to measureElement (it does
        // NOT auto-discover rendered rows — measureElement is the SOLE caller of
        // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
        // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
        // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
        // so the new <tr> set is in the DOM before we measure. Safe from an infinite
        // measure→onChange→measure loop: measureElement is idempotent on an already-observed
        // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
        // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
        // re-measure is a no-op.
        scheduleRemeasure();
      }
    };
  }

  // pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
  // shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
  // two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
  // no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
  // `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
  // guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
  // thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
  // gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
  // Typing-only: the runtime value (a measurement or null) is unchanged.
  function pinMeasurement(pin: number): {
    start: number;
    size: number;
    index: number;
    end: number;
  } | null {
    return pinnedMeasurement(pin);
  }

  // windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
  // { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
  // $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
  // full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
  // `rows` binding → TS2448 self-shadow, line ~1149 lesson).
  function windowedRows() {
    // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
    // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
    // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
    // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
    // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
    // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
    // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
    // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
    // first re-run that picks up the now-non-null virtualizer.
    // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
    // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
    void windowVer();
    void editVer();
    if (!virtualizer) {
      // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
      // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
      // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
      // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
      // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
      // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
      // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
      // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
      // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
      // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
      // the first onChange after _didMount.
      if (!rowsWindowed()) {
        const rowList = rows() || [];
        return rowList.map((r: any, i: any) => ({
          vi: {
            index: i
          },
          row: r
        }));
      }
      return [];
    }
    const items = virtualizer.getVirtualItems();
    const rowList = rows() || [];
    // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
    // shrink window where the virtualizer count is stale relative to $data.rows on the async
    // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
    // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
    const out = items.map((vi: any) => ({
      vi,
      row: rowList[vi.index]
    })).filter((wr: any) => wr.row);
    // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
    // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
    // into another full-model row), LEADING the slice when it sits above the window and TRAILING
    // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
    // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
    // into the real windowing.
    const pin = pinnedEditIndex();
    if (pin >= 0 && rowList[pin]) {
      let inWindow = false;
      for (let i = 0; i < items.length; i++) {
        if (items[i].index === pin) {
          inWindow = true;
          break;
        }
      }
      if (!inWindow) {
        const pm = pinMeasurement(pin);
        const firstStart = items.length ? items[0].start : 0;
        const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
        const pinnedEntry = {
          vi: pm != null ? pm : {
            index: pin
          },
          row: rowList[pin],
          pinned: true
        };
        if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
      }
    }
    return out;
  }

  // Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
  // the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
  // (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
  function padTop() {
    // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
    // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
    // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
    void windowVer();
    void editVer();
    if (!rowsWindowed() || !virtualizer) return 0;
    const items = virtualizer.getVirtualItems();
    let pad = items.length ? items[0].start : 0;
    // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
    // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
    // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
    const pin = pinnedEditIndex();
    if (pin >= 0) {
      const pm = pinMeasurement(pin);
      const inWindow = pmIndexInWindow(items, pin);
      if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
    }
    return pad < 0 ? 0 : pad;
  }
  function padBottom() {
    // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
    // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
    // on pin/unpin.
    void windowVer();
    void editVer();
    if (!rowsWindowed() || !virtualizer) return 0;
    const items = virtualizer.getVirtualItems();
    if (!items.length) return 0;
    let pad = virtualizer.getTotalSize() - items[items.length - 1].end;
    // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
    // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
    const pin = pinnedEditIndex();
    if (pin >= 0) {
      const pm = pinMeasurement(pin);
      const inWindow = pmIndexInWindow(items, pin);
      // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
      // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
      // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
      // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
      // the offset comparison only if the measurement lacks an index (defensive).
      const lastItemIdx = items[items.length - 1].index;
      const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
      if (pm && !inWindow && below) {
        // below the window → it trailed the slice; subtract its height from the trailing spacer.
        if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
      }
    }
    return pad < 0 ? 0 : pad;
  }
  // pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
  function pmIndexInWindow(items: any, idx: any) {
    for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
    return false;
  }
  // rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
  // window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
  function rowIsOutsideWindow(r: any) {
    if (!rowsWindowed() || !virtualizer) return false;
    const items = virtualizer.getVirtualItems();
    for (const it of items as any) if (it.index === r) return false;
    return true;
  }

  // ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
  // rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
  // a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
  // returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
  // cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══

  // windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.

  // Windowing instance state (the `let table` precedent — React hoists reassigned
  // module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
  // when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
  // virtualizer observes; remeasurePending dedupes the deferred sweep.
  let virtualizer: any = null;
  let virtualizerCleanup: any = null;
  let gridScrollEl: any = null;
  let remeasurePending = false;
  // Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
  // the option count at that moment, and the last scrollTop already accounted for.
  let scrollEndPinned = false;
  let scrollEndPinnedCount = -1;
  let scrollEndPinnedTop = -1;

  // windowSource(): the windowing.rzts host-contract row source — the FILTERED option
  // set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
  // (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
  // wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
  // — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
  // every framework :key (and every virtual-core measurement key) to `undefined`, which
  // recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
  // row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
  // value, `_opt` the original option (read via wr.row._opt in the windowed template),
  // `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
  // the same wrapped row the count windows over.
  //
  // $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
  // FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
  // would never hit while filtering). The key covers everything the map reads:
  // options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
  // resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
  // Same reference-stability contract the sibling Combobox's filteredOptions carries —
  // virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
  // unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
  const windowSourceCache = {
    keys: null as any[] | null,
    val: null as any
  };
  function windowSource() {
    const __rozieMemoKey = [local.options, query(), local.optionValue, local.optionLabel];
    const __rozieMemoPrev = windowSourceCache.keys;
    if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
      return windowSourceCache.val;
    }
    const __rozieMemoVal = visibleOptions().map((o: any, i: any) => ({
      id: valueOf(o),
      _opt: o,
      _i: i
    }));
    windowSourceCache.keys = __rozieMemoKey;
    windowSourceCache.val = __rozieMemoVal;
    return __rozieMemoVal;
  }
  // D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
  // A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
  // pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
  // reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
  // to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
  function pinnedEditIndex() {
    return -1;
  }
  function pinnedMeasurement(pin: any) {
    return null;
  }

  // D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
  // today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
  // windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
  // `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
  // pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
  // in this plan calls these through a type-narrowing wrapper.
  //
  // GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
  // `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
  // alongside this were REMOVED here — they were dead code shipped on a mistaken premise
  // about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
  // virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
  // from windowing.rzts; none of those functions' bodies reference the column-axis symbols
  // (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
  // `colIsOutsideWindow()` do, and Listbox never imports any of those), so
  // `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
  // 87-16-SUMMARY.md for the verification trail.
  function rowsWindowed() {
    return !!local.virtual;
  }
  // autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
  // gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
  // estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
  // column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
  // — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
  // `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
  function autoMeasureOn() {
    return false;
  }

  // Keep $data.rows === windowSource() so the windowing math indexes the live option set.
  function syncRows() {
    setRows(windowSource());
  }

  // SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
  // variable-height list at the end while the options in view measure taller than their
  // estimate. The view is judged on the DOM, and only at a move the USER made — a move is
  // virtual-core's own when it still holds an unreconciled scroll adjustment
  // (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
  // computed from its last-observed (stale) offset, so it pulls the view back up from the end
  // and must neither clear the pin nor be mistaken for the user leaving the end. That position
  // is remembered so the scroll event that later reports it is not read as a user move either.
  // (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
  // with every option measured in the ResizeObserver batch while its offset stays at the stale
  // value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
  function recordScrollEnd() {
    if (!virtualizer || !gridScrollEl || virtualizer.scrollState) return;
    const top = gridScrollEl.scrollTop;
    if (top === scrollEndPinnedTop) return;
    scrollEndPinnedTop = top;
    if (virtualizer.scrollAdjustments !== 0) return;
    // Only a list that actually overflows has an end to hold: while the window has not painted
    // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
    // recorded then would jump the freshly opened list to the bottom.
    const sh = gridScrollEl.scrollHeight;
    const ch = gridScrollEl.clientHeight;
    scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
    scrollEndPinnedCount = windowSource().length;
  }

  // Re-apply the pin after the framework has committed the window (called from the rAF pass):
  // the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
  // flight, and not when the option count changed since the user reached the end (a new filter
  // or appended options must not be auto-followed).
  function keepScrollEnd() {
    if (!scrollEndPinned || !virtualizer || !gridScrollEl || virtualizer.scrollState) return;
    if (windowSource().length !== scrollEndPinnedCount) return;
    const maxTop = gridScrollEl.scrollHeight - gridScrollEl.clientHeight;
    if (maxTop - gridScrollEl.scrollTop > 1) {
      gridScrollEl.scrollTop = maxTop;
      scrollEndPinnedTop = gridScrollEl.scrollTop;
    }
  }

  // Defer remeasureWindow() until AFTER the framework commits the recycled window
  // (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
  // behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
  // per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
  // SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
  // budget), the rAF catches React's async commit. measureElement is idempotent on an
  // already-observed node, so running both is cheap and loop-free.
  function scheduleRemeasure() {
    recordScrollEnd();
    if (remeasurePending) return;
    remeasurePending = true;
    let ranMicro = false;
    const microPass = () => {
      remeasureWindow();
    };
    // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
    // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
    // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
    // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
    // committed options cover the virtualizer's window, bounded (the data-table host twin).
    let rafAttempts = 0;
    const rafPass = () => {
      const covered = remeasureWindow();
      rafAttempts = rafAttempts + 1;
      if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
        requestAnimationFrame(rafPass);
        return;
      }
      keepScrollEnd();
      remeasurePending = false;
    };
    if (typeof queueMicrotask !== 'undefined') {
      ranMicro = true;
      queueMicrotask(microPass);
    }
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) remeasurePending = false;else setTimeout(rafPass, 0);
  }

  // measureElement sweep: hand every rendered windowed option to the virtualizer so its
  // true (variable) height is observed (virtual-core measures ONLY nodes passed to
  // measureElement, keyed by the data-index attribute). Bails during a programmatic
  // scroll (scrollToIndex) so a measure can't starve the scroll target.
  function remeasureWindow() {
    if (!virtualizer || !gridScrollEl) return true;
    if (virtualizer.scrollState) return true;
    const els = gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]');
    const rendered = new Set();
    for (const el of els as any) {
      virtualizer.measureElement(el);
      rendered.add(el.getAttribute('data-index'));
    }
    // N-05: false while the framework has not yet committed the recycled window.
    const items = virtualizer.getVirtualItems();
    for (let i = 0; i < items.length; i++) {
      if (!rendered.has(String(items[i].index))) return false;
    }
    return true;
  }

  // ---- focus / scroll helpers (post-mount $refs only) --------------------
  // Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
  // only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
  // verb would override the inherited HTMLElement.focus method on the Lit element.
  function focusControl() {
    triggerElRef?.focus();
  }

  // Keep the active option visible inside the scrolling listbox. Reads $refs in
  // a post-mount callback only (never eagerly — ROZ123). When windowing, route through
  // the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
  // scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
  function scrollActiveIntoView() {
    if (activeIndex() < 0) return;
    if (local.virtual && virtualizer) {
      // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
      // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
      // leave it just-unrendered for a frame on the fine-grained targets (Solid).
      virtualizer.scrollToIndex(activeIndex(), {
        align: 'center'
      });
      scheduleRemeasure();
      return;
    }
    if (!listElRef) return;
    const el = listElRef!.querySelector('#' + CSS.escape(optionId(activeIndex())));
    el?.scrollIntoView({
      block: 'nearest'
    });
  }

  // ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
  // kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
  // re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
  // (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
  // re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
  // asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
  // (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
  // scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
  // (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
  // paints (or attempts run out) — idempotent + loop-free.
  function kickWindow(attempts: any) {
    if (!virtualizer) return;
    gridScrollEl = __rozieRootRef! ? __rozieRootRef!.querySelector('.rozie-listbox-list') : gridScrollEl;
    // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
    // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
    // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
    // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
    if (windowSource().length > 0) {
      syncRows();
      virtualizer.setOptions(virtualizerOptions());
    }
    virtualizer._willUpdate();
    setWindowVer(windowVer() + 1);
    remeasureWindow();
    if (windowedRows().length === 0 && attempts > 0) {
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => kickWindow(attempts - 1));else setTimeout(() => kickWindow(attempts - 1), 16);
    }
  }

  // nextAutoId(): a page-wide counter shared by every Rozie component instance. It
  // lives on globalThis (read through Reflect, which type-checks in the plain-JS and
  // the TS script alike) so separately bundled copies of a leaf never hand out the
  // same id. The same four lines live in Combobox, Listbox and Popover.
  function nextAutoId() {
    const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
    Reflect.set(globalThis, '__rozieAutoId', n);
    return n;
  }

  // idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
  // pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
  function idRoot() {
    return local.id || autoId() || 'rozie-listbox';
  }

  createOutsideClick(
    [() => controlElRef, () => listElRef],
    close,
    () => open$local(),
  );

  return (
    <>
    <div ref={(el) => { __rozieRootRef = el as HTMLElement; }} {...attrs} class={"rozie-listbox" + " " + rozieClass({ 'rozie-listbox-open': open$local(), 'rozie-listbox-disabled': local.disabled, 'rozie-listbox-inline': local.inline }) + (((attrs as unknown as Record<string, unknown>).class as string | undefined) ? " " + ((attrs as unknown as Record<string, unknown>).class as string | undefined) : "")} data-rozie-s-b576227a="">

      
      <div class={"rozie-listbox-control"} ref={(el) => { controlElRef = el as HTMLElement; }} data-rozie-s-b576227a="">
        <button type="button" role="combobox" aria-haspopup="listbox" aria-expanded={open$local()} aria-controls={rozieAttr(idRoot() + '-list')} aria-activedescendant={rozieAttr(activeDescendant())} aria-label={rozieAttr(local.ariaLabel)} ref={(el) => { triggerElRef = el as HTMLElement; }} class={"rozie-listbox-trigger"} disabled={local.disabled} onClick={toggle} onKeyDown={($event: KeyboardEvent & { currentTarget: HTMLButtonElement; target: Element }) => { onControlKeyDown($event); }} data-rozie-s-b576227a="">
          {(_props.selectedSlot ?? _props.slots?.['selected'])?.({ get selected() { return selectedLabel(); }, get value() { return value(); } }) ?? <Show when={selectedLabel()} fallback={<span class={"rozie-listbox-placeholder"} data-rozie-s-b576227a="">{local.placeholder}</span>}><span class={"rozie-listbox-selected"} data-rozie-s-b576227a="">{rozieDisplay(selectedLabel())}</span></Show>}
          <span class={"rozie-listbox-arrow"} aria-hidden="true" data-rozie-s-b576227a="">▾</span>
        </button>
      </div>

      
      {<Show when={open$local() && !local.virtual}><div ref={(el) => { listElRef = el as HTMLElement; }} class={"rozie-listbox-list"} role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={rozieAttr(local.ariaLabel)} aria-multiselectable={local.multiple} data-rozie-s-b576227a="">
        <For each={visibleOptions()}>{(opt, index) => <div role="option" aria-selected={!!isSelected(opt)} aria-disabled={!!disabledOf(opt)} id={rozieAttr(optionId(index()))} class={"rozie-listbox-option" + " " + rozieClass({ 'is-active': activeIndex() === index(), 'is-selected': isSelected(opt), 'is-disabled': disabledOf(opt) })} onClick={($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { select(opt); }} onMouseMove={($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { onOptionPointerMove(index()); }} data-rozie-s-b576227a="">
          {(_props.optionSlot ?? _props.slots?.['option'])?.({ get option() { return opt; }, get index() { return index(); }, get active() { return activeIndex() === index(); }, get selected() { return isSelected(opt); }, get disabled() { return disabledOf(opt); } }) ?? rozieDisplay(labelOf(opt))}
        </div>}</For>

        {<Show when={visibleOptions().length === 0}><div class={"rozie-listbox-empty"} role="presentation" data-rozie-s-b576227a="">
          {(_props.emptySlot ?? _props.slots?.['empty'])?.({ get query() { return query(); } }) ?? "No options"}
        </div></Show>}</div></Show>}{<Show when={local.virtual}><div ref={(el) => { listElRef = el as HTMLElement; }} class={"rozie-listbox-list rozie-listbox-list--virtual"} role="listbox" id={rozieAttr(idRoot() + '-list')} aria-label={rozieAttr(local.ariaLabel)} aria-multiselectable={local.multiple} style={parseInlineStyle((open$local() ? '' : 'display:none;') + (local.maxHeight ? 'height:' + local.maxHeight + ';max-height:' + local.maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + local.maxHeight : 'overflow-y:auto'))} data-rozie-s-b576227a="">
        <div class={"rozie-listbox-spacer"} aria-hidden="true" style={parseInlineStyle('height:' + padTop() + 'px')} data-rozie-s-b576227a="" />

        <Key each={windowedRows() as readonly any[]} by={(wr) => wr.row.id}>{(wr) => <div data-index={rozieAttr(wr().vi.index)} role="option" aria-selected={!!isSelected(wr().row._opt)} aria-disabled={!!disabledOf(wr().row._opt)} id={rozieAttr(optionId(wr().vi.index))} class={"rozie-listbox-option" + " " + rozieClass({ 'is-active': activeIndex() === wr().vi.index, 'is-selected': isSelected(wr().row._opt), 'is-disabled': disabledOf(wr().row._opt) })} onClick={($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { select(wr().row._opt); }} onMouseMove={($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { onOptionPointerMove(wr().vi.index); }} data-rozie-s-b576227a="">
          {(_props.optionSlot ?? _props.slots?.['option'])?.({ get option() { return wr().row._opt; }, get index() { return wr().vi.index; }, get active() { return activeIndex() === wr().vi.index; }, get selected() { return isSelected(wr().row._opt); }, get disabled() { return disabledOf(wr().row._opt); } }) ?? rozieDisplay(labelOf(wr().row._opt))}
        </div>}</Key>

        <div class={"rozie-listbox-spacer"} aria-hidden="true" style={parseInlineStyle('height:' + padBottom() + 'px')} data-rozie-s-b576227a="" />

        {<Show when={windowSource().length === 0}><div class={"rozie-listbox-empty"} role="presentation" data-rozie-s-b576227a="">
          {(_props.emptySlot ?? _props.slots?.['empty'])?.({ get query() { return query(); } }) ?? "No options"}
        </div></Show>}</div></Show>}</div>
    </>
  );
}
ts
import { LitElement, css, html, nothing } from 'lit';
import { customElement, property, query, queryAssignedElements, state } from 'lit/decorators.js';
import { SignalWatcher, effect, signal, untracked } from '@lit-labs/preact-signals';
import { RozieSlotDistributor, attachOutsideClickListener, createLitControllableProperty, rozieAttr, rozieDisplay, rozieListeners, rozieSpread, rozieStyle } from '@rozie/runtime-lit';
import { repeat } from 'lit/directives/repeat.js';
// virtual-core: the framework-agnostic windowing state machine (the data-table
// precedent — NO per-framework adapter). The static import is emitted unconditionally
// (a peer dep); every RUNTIME reference sits behind `if ($props.virtual)` / a
// `virtualizer` guard so the non-virtual emitted path executes none of it
// (byte-identical-off).
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';

// Windowing instance state (the `let table` precedent — React hoists reassigned
// module-`let`s to useRef; do NOT const). NULL until $onMount, and ONLY constructed
// when $props.virtual. gridScrollEl is the captured .rozie-listbox-list scroll div the
// virtualizer observes; remeasurePending dedupes the deferred sweep.

interface RozieSelectedSlotCtx {
  selected: any;
  value: any;
}

interface RozieOptionSlotCtx {
  option: any;
  index: any;
  active: any;
  selected: any;
  disabled: any;
}

interface RozieEmptySlotCtx {
  query: any;
}

@customElement('rozie-listbox')
export default class Listbox extends SignalWatcher(LitElement) {
  static shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, slotAssignment: 'manual' };

  static styles = css`
:host{display:contents}
.rozie-listbox[data-rozie-s-b576227a] {
  position: relative;
  display: inline-block;
  min-width: var(--rozie-listbox-min-width, var(--rlb-min-width, 12rem));
  font: var(--rozie-listbox-font, inherit);
}
.rozie-listbox-control[data-rozie-s-b576227a] { display: block; }
.rozie-listbox-input[data-rozie-s-b576227a],
.rozie-listbox-trigger[data-rozie-s-b576227a] {
  box-sizing: border-box;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
  padding: var(--rozie-listbox-control-padding, var(--rlb-control-padding, 0.5rem 0.75rem));
  font: inherit;
  text-align: left;
  background: var(--rozie-listbox-bg, var(--rlb-bg, #fff));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-border, var(--rlb-border, rgba(0, 0, 0, 0.2)));
  border-radius: var(--rozie-listbox-radius, var(--rlb-radius, 6px));
  cursor: pointer;
}
.rozie-listbox-input[data-rozie-s-b576227a] { cursor: text; }
.rozie-listbox-input[data-rozie-s-b576227a]:focus-visible,
.rozie-listbox-input[data-rozie-s-b576227a]:focus,
.rozie-listbox-trigger[data-rozie-s-b576227a]:focus-visible,
.rozie-listbox-trigger[data-rozie-s-b576227a]:focus {
  outline: var(--rozie-listbox-ring-width, var(--rlb-ring-width, 2px)) solid var(--rozie-listbox-ring, var(--rozie-listbox-accent, var(--rlb-ring, var(--rlb-accent, #0066cc))));
  outline-offset: var(--rozie-listbox-ring-offset, var(--rlb-ring-offset, 1px));
}
.rozie-listbox-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.6)); pointer-events: none; }
.rozie-listbox-placeholder[data-rozie-s-b576227a] { color: var(--rozie-listbox-placeholder, var(--rlb-placeholder, rgba(0, 0, 0, 0.45))); }
.rozie-listbox-arrow[data-rozie-s-b576227a] {
  font-size: 0.75em;
  color: var(--rozie-listbox-arrow-color, var(--rlb-arrow-color, currentColor));
  opacity: var(--rozie-listbox-arrow-opacity, var(--rlb-arrow-opacity, 0.7));
}
.rozie-listbox-list[data-rozie-s-b576227a] {
  position: absolute;
  z-index: var(--rozie-listbox-z, var(--rlb-z, 1000));
  top: calc(100% + var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px)));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--rozie-listbox-popup-padding, var(--rlb-popup-padding, 0.25rem));
  max-height: var(--rozie-listbox-max-height, var(--rlb-max-height, 16rem));
  overflow-y: auto;
  list-style: none;
  background: var(--rozie-listbox-popup-bg, var(--rozie-listbox-bg, var(--rlb-popup-bg, var(--rlb-bg, #fff))));
  color: var(--rozie-listbox-fg, var(--rlb-fg, #1a1a1a));
  border: var(--rozie-listbox-border-width, var(--rlb-border-width, 1px)) solid var(--rozie-listbox-popup-border, var(--rozie-listbox-border, var(--rlb-popup-border, var(--rlb-border, rgba(0, 0, 0, 0.15)))));
  border-radius: var(--rozie-listbox-popup-radius, var(--rozie-listbox-radius, var(--rlb-popup-radius, var(--rlb-radius, 6px))));
  box-shadow: var(--rozie-listbox-shadow, var(--rlb-shadow, 0 6px 24px rgba(0, 0, 0, 0.12)));
}
.rozie-listbox-inline[data-rozie-s-b576227a] {
  display: block;
  width: 100%;
}
.rozie-listbox-inline[data-rozie-s-b576227a] .rozie-listbox-list[data-rozie-s-b576227a] {
  position: static;
  margin-top: var(--rozie-listbox-popup-offset, var(--rlb-popup-offset, 4px));
  border: none;
  border-radius: 0;
  box-shadow: none;
}
.rozie-listbox-option[data-rozie-s-b576227a] {
  padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.4rem 0.6rem));
  border-radius: var(--rozie-listbox-option-radius, var(--rlb-option-radius, 4px));
  color: var(--rozie-listbox-option-fg, inherit);
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rozie-listbox-gap, var(--rlb-gap, 0.5rem));
}
.rozie-listbox-option.is-active[data-rozie-s-b576227a] {
  background: var(--rozie-listbox-active-bg, var(--rlb-active-bg, rgba(0, 102, 204, 0.12)));
  color: var(--rozie-listbox-active-fg, var(--rlb-active-fg, inherit));
}
.rozie-listbox-option.is-selected[data-rozie-s-b576227a] {
  background: var(--rozie-listbox-selected-bg, var(--rlb-selected-bg, transparent));
  color: var(--rozie-listbox-selected-fg, var(--rlb-selected-fg, inherit));
  font-weight: var(--rozie-listbox-selected-weight, var(--rlb-selected-weight, 600));
}
.rozie-listbox-option.is-selected[data-rozie-s-b576227a]::after {
  content: var(--rozie-listbox-check, var(--rlb-check, '✓'));
  color: var(--rozie-listbox-check-color, var(--rozie-listbox-accent, var(--rlb-check-color, var(--rlb-accent, #0066cc))));
}
.rozie-listbox-option.is-disabled[data-rozie-s-b576227a] { opacity: var(--rozie-listbox-disabled-opacity, var(--rlb-disabled-opacity, 0.45)); cursor: not-allowed; }
.rozie-listbox-empty[data-rozie-s-b576227a] { padding: var(--rozie-listbox-option-padding, var(--rlb-option-padding, 0.5rem 0.6rem)); color: var(--rozie-listbox-empty-fg, var(--rlb-empty-fg, rgba(0, 0, 0, 0.5))); }
.rozie-listbox-spacer[data-rozie-s-b576227a] { margin: 0; padding: 0; border: 0; flex: none; }
.rozie-listbox-list--virtual[data-rozie-s-b576227a] { overflow-anchor: none; }
`;

  /**
   * The option set. Each entry is either a primitive (`string`/`number`) or an object; objects resolve their label, value, and disabled state via the `option*` resolver props, falling back to `.label` / `.value` / `.disabled`.
   */
  @property({ type: Array }) options: any[] = [];
  /**
   * The selected value (two-way `r-model`) — a scalar in single-select, an array of values in multi-select. As the sole `model: true` prop it drives the Angular `ControlValueAccessor`, so a Listbox **is** a form control (`[(ngModel)]` / `[formControl]` bind directly).
   * @example
   * <rozie-listbox .value=${fruit} @value-change=${…} .options=${fruits}></rozie-listbox>
   */
  @property({ type: Object, attribute: 'value' }) _value_attr: unknown = null;
  private _valueControllable = createLitControllableProperty<unknown>({ host: this, eventName: 'value-change', defaultValue: null, initialControlledValue: undefined });
  /**
   * Enable multi-select: `value` becomes an array, selecting an option toggles its membership, and the popup stays open after each commit.
   */
  @property({ type: Boolean, reflect: true }) multiple: boolean = false;
  /**
   * Render the results list in normal flow (static) rather than as an absolutely-positioned popup. Use when embedding the listbox inside an `overflow:hidden` container (e.g. a command palette) so the list is not clipped. Defaults `false` (standalone dropdown behavior).
   */
  @property({ type: Boolean, reflect: true }) inline: boolean = false;
  /**
   * Disable the control entirely. Also sets the Angular `ControlValueAccessor` disabled state.
   */
  @property({ type: Boolean, reflect: true }) disabled: boolean = false;
  /**
   * Placeholder text shown in the empty control.
   */
  @property({ type: String, reflect: true }) placeholder: string = '';
  /**
   * Close the popup after a single-select commit. Defaults `true`; multi-select keeps the popup open regardless of this setting.
   */
  @property({ type: Boolean, reflect: true, attribute: 'close-on-select' }) closeOnSelect: boolean = true;
  /**
   * Resolver override for an object option's display label — `(option) => string`. Falls back to the option's `.label` property.
   */
  @property({ type: Function, attribute: 'option-label' }) optionLabel: ((...args: any[]) => any) | null = null;
  /**
   * Resolver override for an object option's committed value — `(option) => value`. Falls back to the option's `.value` property.
   */
  @property({ type: Function, attribute: 'option-value' }) optionValue: ((...args: any[]) => any) | null = null;
  /**
   * Resolver override marking an option non-selectable — `(option) => boolean`. Falls back to the option's `.disabled` property.
   */
  @property({ type: Function, attribute: 'option-disabled' }) optionDisabled: ((...args: any[]) => any) | null = null;
  /**
   * Stable id base for the ARIA wiring (the listbox id, per-option ids, and `aria-activedescendant`). Leave it empty (the default) and each instance generates a unique id base after mount (`rozie-listbox-<n>`); set it when you need stable, predictable ids.
   */
  @property({ type: String, reflect: true }) id: string = '';
  /**
   * Accessible name for the control when there is no visible `<label for>` pointing at its `id` (`aria-label`).
   */
  @property({ type: String, reflect: true, attribute: 'aria-label' }) ariaLabel: string | null = null;
  /**
   * Opt-in vertical **option windowing** for long lists. When `true`, only the visible slice of options renders inside a bounded scrolling list (leading/trailing spacers preserve the total scroll height), windowing over the filtered option set. Default `false` is byte-identical to a non-windowed listbox. Pair with `inline` + `maxHeight` so the windowed scroll container is bounded.
   */
  @property({ type: Boolean, reflect: true }) virtual: boolean = false;
  /**
   * Estimated option row height (px) seeding the windowing engine before `measureElement` refines actual heights. Only consulted when `virtual` is on.
   */
  @property({ type: Number, reflect: true, attribute: 'estimate-row-height' }) estimateRowHeight: number = 36;
  /**
   * A CSS length string bounding the list scroll container when `virtual` is on (e.g. `'320px'`). Mirrored to the `--rozie-listbox-max-height` custom property; the prop wins, the token is the fallback. Ignored when `virtual` is off.
   */
  @property({ type: String, reflect: true, attribute: 'max-height' }) maxHeight: string = '';
  private _autoId = signal('');
  private _open$local = signal(false);
  private _activeIndex = signal(-1);
  private _query = signal('');
  private _rows = signal<any[]>([]);
  private _windowVer = signal(0);
  private _editVer = signal(0);
  @query('[data-rozie-ref="controlEl"]') private _refControlEl!: HTMLElement;
  @query('[data-rozie-ref="triggerEl"]') private _refTriggerEl!: HTMLElement;
  @query('[data-rozie-ref="listEl"]') private _refListEl!: HTMLElement;
  @query('[data-rozie-ref="__rozieRoot"]') private _ref__rozieRoot!: HTMLElement;
private __rozieWatchInitial_0 = true;

  private _rozieSlotDistributor = new RozieSlotDistributor(this);

  @state() private _hasSlotSelected = false;
  @queryAssignedElements({ slot: 'selected', flatten: true }) private _slotSelectedElements!: Element[];
  @property({ attribute: false }) selected?: (scope: { selected: any; value: any }) => unknown;
  @state() private _hasSlotOption = false;
  @queryAssignedElements({ slot: 'option', flatten: true }) private _slotOptionElements!: Element[];
  @property({ attribute: false }) option?: (scope: { option: any; index: any; active: any; selected: any; disabled: any }) => unknown;
  @state() private _hasSlotEmpty = false;
  @queryAssignedElements({ slot: 'empty', flatten: true }) private _slotEmptyElements!: Element[];
  @property({ attribute: false }) empty?: (scope: { query: any }) => unknown;
  // Phase 79 Plan 08 (R4) contract for 79-09: the record intake for
  // record-routed slot fills. 79-09's consumer-side emitSlotFiller
  // accumulates an object literal onto the SAME `.rozieSlots=${{ ... }}`
  // open-tag binding; the KEY is the fill's authored (possibly
  // non-identifier) name and the VALUE is a scope-taking render
  // function. `rozieSlots?.[name]` must be checked BEFORE the legacy
  // named function-prop / <slot> fallback (AC-9). Attribute
  // deserialization is disabled — this is a function-valued record,
  // never reflected to/from an HTML attribute.
  @property({ attribute: false }) rozieSlots?: Record<string, (scope: any) => unknown>;

  private _disconnectCleanups: Array<() => void> = [];
  // Re-parenting guard: set true once the deferred teardown has actually
  // run (a genuine un-mount), so a subsequent reconnect knows to re-arm.
  private _rozieTornDown = false;

  private _armListeners(): void {
    const _u0 = attachOutsideClickListener([() => this._refControlEl, () => this._refListEl], ($event) => {  ((this.close) as (...args: any[]) => any)($event); }, () => (this._open$local.value));
    this._disconnectCleanups.push(_u0);

    {
      const slotEl = this.shadowRoot?.querySelector('slot[name="selected"]');
      if (slotEl !== null && slotEl !== undefined) {
        const update = () => { this._hasSlotSelected = this._slotSelectedElements.length > 0; };
        slotEl.addEventListener('slotchange', update);
        // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
        this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
        update();
      }
    }

    {
      const slotEl = this.shadowRoot?.querySelector('slot[name="option"]');
      if (slotEl !== null && slotEl !== undefined) {
        const update = () => { this._hasSlotOption = this._slotOptionElements.length > 0; };
        slotEl.addEventListener('slotchange', update);
        // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
        this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
        update();
      }
    }

    {
      const slotEl = this.shadowRoot?.querySelector('slot[name="empty"]');
      if (slotEl !== null && slotEl !== undefined) {
        const update = () => { this._hasSlotEmpty = this._slotEmptyElements.length > 0; };
        slotEl.addEventListener('slotchange', update);
        // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
        this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
        update();
      }
    }
  }

  connectedCallback(): void {
    // Phase 07.3.1 D-LIT-15 — pre-seed _hasSlot<X> from light DOM so first render isn't deadlocked.
    this._hasSlotSelected = Array.from(this.children).some((el) => el.getAttribute('slot') === 'selected');
    this._hasSlotOption = Array.from(this.children).some((el) => el.getAttribute('slot') === 'option');
    this._hasSlotEmpty = Array.from(this.children).some((el) => el.getAttribute('slot') === 'empty');
    super.connectedCallback();
    if (this.hasUpdated && this._rozieTornDown) { this._rozieTornDown = false; this._armListeners(); }
  }

  firstUpdated(): void {
    this._armListeners();

    this._disconnectCleanups.push(effect(() => { const __watchVal = (() => (this.options ? this.options.length : 0) + '|' + this._query.value)(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => {
      this.syncRows();
      if (this.virtual && this.virtualizer) {
        this.gridScrollEl = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rozie-listbox-list') : this.gridScrollEl;
        this.virtualizer.setOptions(this.virtualizerOptions());
        this.virtualizer._willUpdate();
        this._windowVer.value = this._windowVer.value + 1;
        this.scheduleRemeasure();
      }
    })(); }); }));

    if (!this.id) this._autoId.value = 'rozie-listbox-' + this.nextAutoId();
    this.syncRows();
    if (this.virtual) {
      // The list renders at mount when virtual, so the .rozie-listbox-list scroll container
      // exists here. Capture it via $el.querySelector (the data-table gridScrollEl precedent,
      // proven ×6 incl Lit shadow + Solid) — $refs on a conditionally-rendered node is null on
      // Solid/Lit, which leaves the virtualizer with no scroll element.
      this.gridScrollEl = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rozie-listbox-list') : null;
      this.virtualizer = new Virtualizer(this.virtualizerOptions());
      this.virtualizerCleanup = this.virtualizer._didMount();
      this._windowVer.value = this._windowVer.value + 1;
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => this.kickWindow(8));else setTimeout(() => this.kickWindow(8), 0);
    }
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    queueMicrotask(() => {
      if (this.isConnected || this._rozieTornDown) return;
      this._rozieTornDown = true;
      (() => {
        if (this.typeTimer !== null) clearTimeout(this.typeTimer);
        // Tear down the virtualizer's scroll-element ResizeObserver (no-op when virtual off).
        if (this.virtualizerCleanup) this.virtualizerCleanup();
      })();
      for (const fn of this._disconnectCleanups) fn();
      this._disconnectCleanups = [];
    });
  }

  attributeChangedCallback(name: string, old: string | null, value: string | null): void {
    super.attributeChangedCallback(name, old, value);
    if (name === 'value') this._valueControllable.notifyAttributeChange(value as unknown as unknown);
  }

  render() {
    return html`
<div class="${Object.entries({ "rozie-listbox": true, 'rozie-listbox-open': this._open$local.value, 'rozie-listbox-disabled': this.disabled, 'rozie-listbox-inline': this.inline }).filter(([, v]) => v).map(([k]) => k).join(' ')}" ${rozieSpread(this.$attrs)} ${rozieListeners(this.$listeners)} data-rozie-ref="__rozieRoot" data-rozie-s-b576227a>

  
  <div class="rozie-listbox-control" data-rozie-ref="controlEl" data-rozie-s-b576227a>
    <button class="rozie-listbox-trigger" type="button" role="combobox" aria-haspopup="listbox" aria-expanded=${this._open$local.value} aria-controls=${rozieAttr(this.idRoot() + '-list')} aria-activedescendant=${rozieAttr(this.activeDescendant)} aria-label=${rozieAttr(this.ariaLabel)} ?disabled=${this.disabled} @click=${this.toggle} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onControlKeyDown($event); }} data-rozie-ref="triggerEl" data-rozie-s-b576227a>
      ${this.selected !== undefined ? this.selected({selected: this.selectedLabel, value: this.value}) : html`<slot name="selected" data-rozie-params=${(() => { try { return JSON.stringify({selected: this.selectedLabel, value: this.value}); } catch { return '{}'; } })()}>
        ${this.selectedLabel ? html`<span class="rozie-listbox-selected" data-rozie-s-b576227a>${rozieDisplay(this.selectedLabel)}</span>` : html`<span class="rozie-listbox-placeholder" data-rozie-s-b576227a>${this.placeholder}</span>`}</slot>`}
      <span class="rozie-listbox-arrow" aria-hidden="true" data-rozie-s-b576227a>▾</span>
    </button>
  </div>

  
  ${this._open$local.value && !this.virtual ? html`<div class="rozie-listbox-list" role="listbox" id=${rozieAttr(this.idRoot() + '-list')} aria-label=${rozieAttr(this.ariaLabel)} aria-multiselectable=${this.multiple} data-rozie-ref="listEl" data-rozie-s-b576227a>
    ${repeat<any>(this.visibleOptions(), (opt, index) => this.optionId(index), (opt, index) => html`<div class="${Object.entries({ "rozie-listbox-option": true, 'is-active': this._activeIndex.value === index, 'is-selected': this.isSelected(opt), 'is-disabled': this.disabledOf(opt) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" id=${rozieAttr(this.optionId(index))} role="option" aria-selected=${!!this.isSelected(opt)} aria-disabled=${!!this.disabledOf(opt)} @click=${($event: MouseEvent & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.select(opt); }} @mousemove=${($event: MouseEvent & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.onOptionPointerMove(index); }} data-rozie-s-b576227a>
      ${this.option !== undefined ? this.option({option: opt, index: index, active: this._activeIndex.value === index, selected: this.isSelected(opt), disabled: this.disabledOf(opt)}) : html`<slot name="option" data-rozie-params=${(() => { try { return JSON.stringify({option: opt, index: index, active: this._activeIndex.value === index, selected: this.isSelected(opt), disabled: this.disabledOf(opt)}); } catch { return '{}'; } })()}>
        ${rozieDisplay(this.labelOf(opt))}
      </slot>`}
    </div>`)}

    ${this.visibleOptions().length === 0 ? html`<div class="rozie-listbox-empty" role="presentation" data-rozie-s-b576227a>
      ${this.empty !== undefined ? this.empty({query: this._query.value}) : html`<slot name="empty" data-rozie-params=${(() => { try { return JSON.stringify({query: this._query.value}); } catch { return '{}'; } })()}>No options</slot>`}
    </div>` : nothing}</div>` : nothing}${this.virtual ? html`<div class="rozie-listbox-list rozie-listbox-list--virtual" role="listbox" id=${rozieAttr(this.idRoot() + '-list')} aria-label=${rozieAttr(this.ariaLabel)} aria-multiselectable=${this.multiple} style=${rozieStyle((this._open$local.value ? '' : 'display:none;') + (this.maxHeight ? 'height:' + this.maxHeight + ';max-height:' + this.maxHeight + ';overflow-y:auto;--rozie-listbox-max-height:' + this.maxHeight : 'overflow-y:auto'))} data-rozie-ref="listEl" data-rozie-s-b576227a>
    <div class="rozie-listbox-spacer" aria-hidden="true" style=${rozieStyle('height:' + this.padTop() + 'px')} data-rozie-s-b576227a></div>

    ${repeat<any>(this.windowedRows(), (wr, _idx) => wr.row.id, (wr, _idx) => html`<div class="${Object.entries({ "rozie-listbox-option": true, 'is-active': this._activeIndex.value === wr.vi.index, 'is-selected': this.isSelected(wr.row._opt), 'is-disabled': this.disabledOf(wr.row._opt) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" id=${rozieAttr(this.optionId(wr.vi.index))} data-index=${rozieAttr(wr.vi.index)} role="option" aria-selected=${!!this.isSelected(wr.row._opt)} aria-disabled=${!!this.disabledOf(wr.row._opt)} @click=${($event: MouseEvent & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.select(wr.row._opt); }} @mousemove=${($event: MouseEvent & { currentTarget: HTMLDivElement; target: HTMLDivElement }) => { this.onOptionPointerMove(wr.vi.index); }} data-rozie-s-b576227a>
      ${this.option !== undefined ? this.option({option: wr.row._opt, index: wr.vi.index, active: this._activeIndex.value === wr.vi.index, selected: this.isSelected(wr.row._opt), disabled: this.disabledOf(wr.row._opt)}) : html`<slot name="option" data-rozie-params=${(() => { try { return JSON.stringify({option: wr.row._opt, index: wr.vi.index, active: this._activeIndex.value === wr.vi.index, selected: this.isSelected(wr.row._opt), disabled: this.disabledOf(wr.row._opt)}); } catch { return '{}'; } })()}>
        ${rozieDisplay(this.labelOf(wr.row._opt))}
      </slot>`}
    </div>`)}

    <div class="rozie-listbox-spacer" aria-hidden="true" style=${rozieStyle('height:' + this.padBottom() + 'px')} data-rozie-s-b576227a></div>

    ${this.windowSource().length === 0 ? html`<div class="rozie-listbox-empty" role="presentation" data-rozie-s-b576227a>
      ${this.empty !== undefined ? this.empty({query: this._query.value}) : html`<slot name="empty" data-rozie-params=${(() => { try { return JSON.stringify({query: this._query.value}); } catch { return '{}'; } })()}>No options</slot>`}
    </div>` : nothing}</div>` : nothing}</div>
`;
  }

  // Type-ahead buffer for the select-only listbox trigger. Module-scope
  // `let`s reassigned from handlers → the React emitter hoists them to `useRef`
  // so they persist across renders (the setup-once guarantee); no-op elsewhere.
  // They STAY in this host (not the shared spine) per the A==B rule: reassigned
  // module-`let`s + sigils live in the host; the partial only closes over them.
  typeBuffer = '';

  typeTimer: any = null;

  // ---- shared list spine (P2: @rozie-ui/headless-core/listCore.rzts) ------
  // The option resolvers, client filter, enabled-index navigation, the keyboard
  // reducer, type-ahead, single+multi selection, open/close state, and
  // activeDescendant derivation now live in the shared, focus-/input-mode
  // parameterized list spine. It is a compile-time `.rzts` script-partial: it
  // dissolves into this leaf via inlineScriptPartials() before IR lowering (zero
  // runtime dep). Listbox consumes it in focus-model `activedescendant` +
  // input-mode `select-only` + multi + type-ahead. The spine closes over this
  // host's pieces by convention: the reassigned module-`let`s typeBuffer/typeTimer
  // (above) and the impure ref fns focusControl/scrollActiveIntoView (below).
  // ══ Shared headless LIST SPINE (Phase 64, D-06) — the target-agnostic list-core bridge ══
  // Lifted verbatim from Listbox.rozie's <script> (the monolithic pure-Rozie list logic). This
  // partial holds ONLY the PURE list spine — option resolvers, the client-side filter, enabled-index
  // navigation, the arrow/home/end/enter/escape/space/tab keyboard reducer, type-ahead, single+multi
  // selection, open/close state, and activeDescendant derivation. It is a compile-time `.rzts`
  // script-partial: it dissolves into each consumer's compiled leaf via inlineScriptPartials() before
  // IR lowering — leaving zero runtime dependency (the 64-01-proven cross-package bare-specifier path).
  //
  // ── PARAMETERIZATION (D-06) ──────────────────────────────────────────────────────────────────
  // The spine is parameterized BY HOST CONVENTION (the same implicit by-convention mixin contract
  // windowing.rzts uses) along two axes:
  //   - focus-model: `activedescendant` | `roving`. Both list families default to `activedescendant`
  //     (what they use today): the highlighted option is tracked virtually via `activeDescendant`
  //     (an option id) while DOM focus stays on the control. `roving` (real per-option tabindex
  //     focus) is SUPPORTED-BUT-UNUSED — no focus rewrite is forced here; a roving host would supply
  //     its own focus mover. The `activeDescendant` / `optionId` derivation below IS the
  //     activedescendant model.
  //   - input-mode: `select-only` (Listbox — a button trigger + type-ahead) | `filter-input`
  //     (Combobox — a text <input> that filters by the typed query). The mode is by HOST CONVENTION,
  //     NOT a discriminant prop (P3 retired the Listbox `combobox`/`filterable` props): a select-only
  //     host never writes `$data.query`, so `visibleOptions` is the identity path for it and the
  //     printable-char branch of the reducer feeds type-ahead; a filter-input host writes `$data.query`
  //     from its <input>, so `visibleOptions` substring-filters and `onInput` drives the query.
  //
  // ── HOST CONTRACT (symbols the consuming host MUST define before importing) ────────────────────
  //   - the reassigned module-`let`s `typeBuffer` / `typeTimer` — type-ahead scratch state. They are
  //     reassigned from handlers → the React emitter hoists them to `useRef` (the setup-once
  //     guarantee), so per the A==B playbook rule they STAY IN THE HOST; this partial only closes
  //     over them (in `onTypeahead`).
  //   - `idRoot()` — the host's id base (Listbox: the `id` prop, else the per-instance id it
  //     generates in $onMount); `optionId` below derives every option id from it.
  //   - `focusControl()` / `scrollActiveIntoView()` — impure ref-reading functions (they touch the
  //     control / list ref elements, which are post-mount-only per ROZ123), so they are per-consumer
  //     HOST functions; this partial only closes over them (it reads NO refs itself).
  //   - the option set + form surface (`$props.options` / `$props.value` (model) / `$props.multiple` /
  //     `$props.optionLabel` / `$props.optionValue` / `$props.optionDisabled` /
  //     `$props.closeOnSelect` / `$props.disabled`) and the reactive state (`$data.open` /
  //     `$data.activeIndex` / `$data.query`). Input-mode is by convention (the host's <input> writing
  //     `$data.query`), NOT a discriminant prop.
  // ---- option resolvers --------------------------------------------------
  labelOf = (opt: any) => {
  if (this.optionLabel !== null) return this.optionLabel(opt);
  if (opt !== null && typeof opt === 'object' && 'label' in opt) return opt.label;
  return String(opt);
};

  valueOf$local = (opt: any) => {
  if (this.optionValue !== null) return this.optionValue(opt);
  if (opt !== null && typeof opt === 'object' && 'value' in opt) return opt.value;
  return opt;
};

  disabledOf = (opt: any) => {
  if (this.optionDisabled !== null) return !!this.optionDisabled(opt);
  if (opt !== null && typeof opt === 'object' && 'disabled' in opt) return !!opt.disabled;
  return false;
};

  // `idRoot()` is a HOST function (the host's id base: its id prop, else a generated
  // per-instance id) so the option ids follow the host's auto-id fallback.
  optionId = (index: any) => this.idRoot() + '-opt-' + index;

  // ---- derived state -----------------------------------------------------
  // The visible option list: identity in select-only / non-filtering mode,
  // a case-insensitive substring filter when a combobox query is present.
  // A plain function (not `$computed`) so it reads uniformly across all six
  // targets — a `$computed` is a value on React but an accessor on Solid, so
  // aliasing it to a local (`const opts = visibleOptions()`) diverges; calling a
  // plain function is identical everywhere.
  visibleOptions = () => {
  const q = (this._query.value || '').trim().toLowerCase();
  if (q === '') return this.options;
  return this.options.filter((opt: any) => this.labelOf(opt).toLowerCase().includes(q));
};

  // The label shown in the (select-only) trigger when closed. A real `$computed`
  // — read bare in the template, never aliased in script, so the per-target
  // accessor form stays uniform.
  get selectedLabel() {
    const cur = this.value;
    if (this.multiple) {
      // Read the model value into a local before narrowing: `$props.value` lowers
      // to a `value()` accessor on Solid, and Array.isArray() can't narrow two
      // separate calls — narrowing one stable local works on every target.
      const arr = Array.isArray(cur) ? cur : [];
      if (arr.length === 0) return '';
      return this.options.filter((o: any) => arr.includes(this.valueOf$local(o))).map(this.labelOf).join(', ');
    }
    const match = this.options.find((o: any) => this.valueOf$local(o) === cur);
    return match === undefined ? '' : this.labelOf(match);
  }

  // The id of the active option, for aria-activedescendant. null when none.
  get activeDescendant() {
    if (!this._open$local.value || this._activeIndex.value < 0) return null;
    return this.optionId(this._activeIndex.value);
  }

  // Is a given option currently selected? Multi compares array membership.
  isSelected = (opt: any) => {
  const v = this.valueOf$local(opt);
  const cur = this.value;
  if (this.multiple) return Array.isArray(cur) && cur.includes(v);
  return cur === v;
};

  // First enabled visible index, preferring the currently-selected option.
  resolveInitialActive = () => {
  const opts = this.visibleOptions();
  const sel = opts.findIndex((o: any) => this.isSelected(o) && !this.disabledOf(o));
  if (sel !== -1) return sel;
  return opts.findIndex((o: any) => !this.disabledOf(o));
};

  // ---- open / close ------------------------------------------------------
  // Phase 73 item #8 (emitter-hardening batch): each of `open`/`close`/`toggle`
  // $emit's directly — no longer funneled through a single wrapper. The
  // former "route every emit through ONE wrapper fn" workaround guarded
  // against a React duplicate `const {onOpenChange}=props` per emit-site
  // (TS2451); verified against the current emitter (target-react
  // `emitScript-multiEmitDedupe.test.ts`) that the shipped ITEM-1 (Phase 46)
  // hoist-once dedupe already collapses N ESCAPING helpers sharing an emit
  // target into exactly one destructure, and a non-escaping function (e.g.
  // `open`, reachable here only via `$expose`) never destructures at all — so
  // no combination of these three functions can produce the duplicate-const
  // shape. See project_next_port_listbox / project_emitter_hardening_backlog.
  open = () => {
  if (this.disabled) return;
  if (this._open$local.value) return;
  this._open$local.value = true;
  this._activeIndex.value = this.resolveInitialActive();
  this.dispatchEvent(new CustomEvent("open-change", {
    detail: {
      open: true
    },
    bubbles: true,
    composed: true
  }));
};

  close = () => {
  if (!this._open$local.value) return;
  this._open$local.value = false;
  this._activeIndex.value = -1;
  this.dispatchEvent(new CustomEvent("open-change", {
    detail: {
      open: false
    },
    bubbles: true,
    composed: true
  }));
};

  toggle = () => {
  if (this._open$local.value) this.close();else this.open();
};

  // ---- selection ---------------------------------------------------------
  select = (opt: any) => {
  if (this.disabledOf(opt)) return;
  const v = this.valueOf$local(opt);
  if (this.multiple) {
    const cur = this.value;
    const arr = Array.isArray(cur) ? cur : [];
    // Fresh array on every commit — in-place mutation is dropped by the
    // React/Solid/Lit/Angular change detectors.
    const next = arr.includes(v) ? arr.filter((x: any) => x !== v) : [...arr, v];
    this._valueControllable.write(next);
    this.dispatchEvent(new CustomEvent("change", {
      detail: {
        value: next,
        option: opt
      },
      bubbles: true,
      composed: true
    }));
  } else {
    this._valueControllable.write(v);
    this.dispatchEvent(new CustomEvent("change", {
      detail: {
        value: v,
        option: opt
      },
      bubbles: true,
      composed: true
    }));
    if (this.closeOnSelect) {
      this.close();
      this.focusControl();
    }
  }
};

  clear = () => {
  const empty = this.multiple ? [] : null;
  this._valueControllable.write(empty);
  this._query.value = '';
  this.dispatchEvent(new CustomEvent("change", {
    detail: {
      value: empty,
      option: null
    },
    bubbles: true,
    composed: true
  }));
};

  // ---- keyboard navigation over the VISIBLE list -------------------------
  nextEnabled = (from: any, dir: any) => {
  const opts = this.visibleOptions();
  if (opts.length === 0) return -1;
  let i = from;
  for (let step = 0; step < opts.length; step++) {
    i += dir;
    if (i < 0) i = opts.length - 1;else if (i >= opts.length) i = 0;
    if (!this.disabledOf(opts[i])) return i;
  }
  return from;
};

  move = (dir: any) => {
  if (!this._open$local.value) {
    this.open();
    return;
  }
  const start = this._activeIndex.value < 0 ? dir > 0 ? -1 : 0 : this._activeIndex.value;
  this._activeIndex.value = this.nextEnabled(start, dir);
  this.scrollActiveIntoView();
};

  moveEdge = (toEnd: any) => {
  if (!this._open$local.value) this.open();
  this._activeIndex.value = toEnd ? this.nextEnabled(-1, -1) : this.nextEnabled(-1, 1);
  this.scrollActiveIntoView();
};

  commitActive = () => {
  const opts = this.visibleOptions();
  if (this._activeIndex.value >= 0 && this._activeIndex.value < opts.length) this.select(opts[this._activeIndex.value]);
};

  // Type-ahead for select-only listboxes: accumulate keystrokes and jump to the
  // first option whose label starts with the buffer.
  onTypeahead = (ch: any) => {
  if (this.typeTimer !== null) clearTimeout(this.typeTimer);
  this.typeBuffer += ch.toLowerCase();
  this.typeTimer = setTimeout(() => {
    this.typeBuffer = '';
  }, 600);
  const opts = this.visibleOptions();
  const idx = opts.findIndex((o: any) => !this.disabledOf(o) && this.labelOf(o).toLowerCase().startsWith(this.typeBuffer));
  if (idx !== -1) {
    if (!this._open$local.value) this.open();
    this._activeIndex.value = idx;
    this.scrollActiveIntoView();
  }
};

  // Key handler shared by the trigger and the combobox input. The printable-
  // character branch is reached only in select-only mode (the combobox input
  // types through @input).
  onControlKeyDown = ($event: any) => {
  const key = $event.key;
  if (key === 'ArrowDown') {
    $event.preventDefault();
    this.move(1);
  } else if (key === 'ArrowUp') {
    $event.preventDefault();
    this.move(-1);
  } else if (key === 'Home') {
    $event.preventDefault();
    this.moveEdge(false);
  } else if (key === 'End') {
    $event.preventDefault();
    this.moveEdge(true);
  } else if (key === 'Enter') {
    if (this._open$local.value) {
      $event.preventDefault();
      this.commitActive();
    }
  } else if (key === 'Escape') {
    if (this._open$local.value) {
      $event.preventDefault();
      this.close();
      this.focusControl();
    }
  } else if (key === ' ' || key === 'Spacebar') {
    // Space toggles / commits in a select-only host (a button trigger). A
    // filter-input host types the literal space into its <input> and does NOT
    // route Space through this reducer, so this branch is select-only by use.
    $event.preventDefault();
    if (!this._open$local.value) this.open();else this.commitActive();
  } else if (key === 'Tab') {
    if (this._open$local.value) this.close();
  } else if (key.length === 1 && !$event.metaKey && !$event.ctrlKey && !$event.altKey) {
    this.onTypeahead(key);
  }
};

  // Combobox input handler: keep the popup open while typing, reset the active
  // highlight to the first match, and surface the query for remote filtering.
  // Pointer hover sets the virtual highlight (matches native <select> feel).
  onOptionPointerMove = (index: any) => {
  if (this._activeIndex.value !== index) this._activeIndex.value = index;
};

  // ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
  // Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
  // holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
  // in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
  // compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
  //
  // HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
  // by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
  //   - windowSource(): T[]   — the full list to window (the KEY generalization; the DataTable host
  //                             returns its pre-pagination row model, listbox/combobox return the
  //                             filtered options). This partial MUST NOT reach into the host data engine
  //                             directly — rows arrive ONLY through windowSource().
  //   - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
  //   - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
  //   - gridScrollEl              — the scroll-container element handle.
  //   - virtualizer               — the host virtual-core instance (built in $onMount from the ref).
  //   - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
  //   - scheduleRemeasure()       — the host's rAF/microtask remeasure defer.
  //   - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
  //                             defaulting to no-op): the DataTable host passes its edit-pinning hooks;
  //                             listbox passes nothing. Routing pinning through this host hook (NOT
  //                             inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
  //   - rowsWindowed(): boolean  — is the ROW axis windowed. REQUIRED, no default — replaces every bare
  //                             truthiness read of the host's windowing prop (D-05); `windowedRows()` /
  //                             `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
  //                             convention exactly as they already call `pinnedEditIndex()`.
  //   - colsWindowed(): boolean  — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
  //                             host until it defines the real column-axis mechanism (87-04+).
  //   - columnCount(): number    — the leaf-column count the column virtualizer windows over. REQUIRED,
  //                             no default.
  //   - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
  //                             from table-core's `getSize()` under D-06. REQUIRED, no default.
  //   - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
  //                             DataTable host unions pinned + active-cell + editing column indices into
  //                             the column-window slice; listbox/combobox pass an empty array (host-
  //                             provided, defaulting to `[]`).
  //   - colVirtualizer           — the host's SECOND virtual-core instance, windowing the COLUMN axis
  //                             (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
  //   - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
  //                             data-table's real body reads `$props.autoMeasure === true`; listbox/
  //                             combobox/command-palette return `false` so the accumulator branch
  //                             estimateRowSize() gates on is dead code for them (D-20).
  //   - afterRowRemeasure        — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
  //                             assigned to refineRowEstimate() (below) by the host. The DataTable
  //                             host's remeasureWindow() (virtualization.rzts) calls it AFTER its
  //                             measureElement sweep so the fold + hysteresis re-feed run on every
  //                             window commit. Routed through a mutable `let` rather than a direct
  //                             call FROM virtualization.rzts INTO this file: a relative-partial CONST
  //                             calling a bare-specifier-partial CONST is the exact forward-reference
  //                             TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
  //                             documents for columnVirtualizerOptions() (inlineScriptPartials()
  //                             groups the relative partial BEFORE the bare-specifier partial in the
  //                             merged per-target output, regardless of source import order). A
  //                             mutable `let` hoists to `useRef` on React and is excluded from a
  //                             useCallback's dependency array, sidestepping the hazard entirely — the
  //                             SAME mechanism `refreshRowModel` already relies on.
  //
  // AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
  // LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
  // note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
  // (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
  // `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
  // `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
  // (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
  // (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
  // (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
  // (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
  // axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
  // sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
  // Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
  // (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
  // hardcoded constant, separate from the row axis's `overscan: 8` below).
  //
  // isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
  // immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
  // signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
  // mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.
  // getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
  // React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
  // id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
  virtualItemKey = (i: any) => {
  const src = this.windowSource();
  return src && src[i] ? src[i].id : undefined;
};

  // COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
  // row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
  // cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
  // overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
  // horizontal Virtualizer instance (see the AXIS MECHANISM note above).
  // ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
  // mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
  // hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
  // `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
  // precedent), so two DataTable instances on one page never share an accumulator
  // (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
  // row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
  // virtual-core (the hysteresis comparison baseline). ══
  measuredRowTotal = 0;

  measuredRowCount = 0;

  // ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
  // write behind a SINGLE microtask-deferred increment, regardless of how many callers request
  // one within the same synchronous JS task. ══
  //
  // ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
  // confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
  // (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
  // `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
  // TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
  // framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
  // (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
  // ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
  // synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
  // measured rows from the flat seed to their real heights) can fire onChange, and therefore an
  // UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
  // refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
  // folded those same measurements into the running mean or re-fed the converged estimate into
  // virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
  // getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
  // resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
  // reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
  // effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
  // resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
  // refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
  // reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
  // synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
  // correct by construction. Solid does not auto-batch a signal write made from outside a
  // Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
  // externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
  // virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
  // getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
  // the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
  // recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
  // further row happens to differ from its cache — so the DOM stays frozen on that stale,
  // pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
  // converged correctly (T-87-07's own confirmed finding).
  //
  // FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
  // refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
  // idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
  // EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
  // fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
  // batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
  // column widths come from table-core's getSize() oracle, never measured from the DOM), so
  // columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
  windowVerBumpPending = false;

  bumpWindowVer = (): void => {
  if (this.windowVerBumpPending) return;
  this.windowVerBumpPending = true;
  const flush = () => {
    this.windowVerBumpPending = false;
    this._windowVer.value = this._windowVer.value + 1;
  };
  // Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
  // (virtualization.rzts) for environments where queueMicrotask is unavailable.
  if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
};

  // ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
  // virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
  // re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
  // own measureElement/resizeItem idempotence (see refineRowEstimate() below).
  // estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
  // FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
  // verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
  // regardless of autoMeasure) still returns the seed — the very first render has nothing
  // measured yet either way (D-15).
  estimateRowSize = (i: number): number => {
  if (!this.autoMeasureOn()) return this.estimateRowHeight;
  if (this.measuredRowCount === 0) return this.estimateRowHeight;
  return Math.round(this.measuredRowTotal / this.measuredRowCount);
};

  // foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
  // accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
  // The FULL virtualizer options. virtual-core's setOptions REPLACES options with
  // `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
  // source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
  // Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
  // on virtual-core's generic option inference. onChange's windowVer write is routed through
  // bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
  // this onChange MANY times in a single synchronous sweep (once per row whose real measured
  // size differs from its cache, e.g. every never-before-measured row in the FIRST window),
  // and coalescing those into one microtask-deferred write is what keeps every target's render
  // landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
  // Solid-specific rendering gap this closes). The React emitter still lowers the underlying
  // `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
  // microtask, exactly as it was correct from a mount closure before.
  virtualizerOptions = (): any => ({
  count: this.windowSource().length,
  getScrollElement: () => this.gridScrollEl,
  estimateSize: (i: any) => this.estimateRowSize(i),
  observeElementRect,
  observeElementOffset,
  scrollToFn: elementScroll,
  measureElement,
  overscan: 8,
  getItemKey: this.virtualItemKey,
  onChange: () => {
    this.bumpWindowVer();
    // CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
    // virtual-core only observe()s a node you explicitly hand to measureElement (it does
    // NOT auto-discover rendered rows — measureElement is the SOLE caller of
    // observer.observe, virtual-core@3.17.1 dist/esm/index.js:794-817). Rows that recycle
    // into view on scroll are brand-new DOM nodes; without re-sweeping they keep the
    // estimateRowHeight seed forever and the spacer math drifts (req-2). Deferred one frame
    // so the new <tr> set is in the DOM before we measure. Safe from an infinite
    // measure→onChange→measure loop: measureElement is idempotent on an already-observed
    // node (the `prevNode !== node` guard), and resizeItem only re-fires onChange when the
    // measured height actually DIFFERS from the cached one (delta !== 0) — an unchanged
    // re-measure is a no-op.
    this.scheduleRemeasure();
  }
});

  // pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
  // shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
  // two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
  // no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
  // `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
  // guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
  // thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
  // gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
  // Typing-only: the runtime value (a measurement or null) is unchanged.
  pinMeasurement = (pin: number): {
  start: number;
  size: number;
  index: number;
  end: number;
} | null => this.pinnedMeasurement(pin);

  // windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
  // { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
  // $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
  // full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
  // `rows` binding → TS2448 self-shadow, line ~1149 lesson).
  windowedRows = () => {
  // SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
  // early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
  // which happens at initial render while `virtualizer` is still null (it is built in $onMount,
  // after the first render). `virtualizer` is a non-reactive `let`, so if the windowVer read sat
  // BELOW the `!virtualizer` guard the accessor would early-return [] without ever reading the
  // signal → it would NEVER re-run when onChange later bumps windowVer, and the window would stay
  // blank forever (the Solid/Svelte fine-grained bug). Coarse targets re-render wholesale so the
  // placement is a no-op for them. The post-construction windowVer bump in $onMount fires the
  // first re-run that picks up the now-non-null virtualizer.
  // ALSO subscribe to editVer here so the slice re-derives when an editor opens/closes (the
  // pin/unpin transition), mirroring the probe's windowVer bump on pin (Solid/Svelte fine-grained).
  void this._windowVer.value;
  void this._editVer.value;
  if (!this.virtualizer) {
    // Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
    // wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
    // virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
    // set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
    // own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
    // output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
    // colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
    // this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
    // Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
    // render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
    // the first onChange after _didMount.
    if (!this.rowsWindowed()) {
      const rowList = this._rows.value || [];
      return rowList.map((r: any, i: any) => ({
        vi: {
          index: i
        },
        row: r
      }));
    }
    return [];
  }
  const items = this.virtualizer.getVirtualItems();
  const rowList = this._rows.value || [];
  // WR-01: drop any virtual item whose index outruns the current full-model rows (a brief
  // shrink window where the virtualizer count is stale relative to $data.rows on the async
  // onChange→windowVer path). The template keys on wr.row.id, so a row:undefined entry would
  // throw "Cannot read properties of undefined"; filter it here so the template never sees it.
  const out = items.map((vi: any) => ({
    vi,
    row: rowList[vi.index]
  })).filter((wr: any) => wr.row);
  // ── D-02 pin-row union (req-9): if an editor is open on a row that is NOT in the current
  // window, UNION it into the slice (keyed on row.id so Lit repeat / Solid For never recycle it
  // into another full-model row), LEADING the slice when it sits above the window and TRAILING
  // it when below — so DOM order matches visual/aria order. The spacer subtraction (padTop/
  // padBottom) keeps the total exactly getTotalSize(). This is the 51-01-proven mechanism wired
  // into the real windowing.
  const pin = this.pinnedEditIndex();
  if (pin >= 0 && rowList[pin]) {
    let inWindow = false;
    for (let i = 0; i < items.length; i++) {
      if (items[i].index === pin) {
        inWindow = true;
        break;
      }
    }
    if (!inWindow) {
      const pm = this.pinMeasurement(pin);
      const firstStart = items.length ? items[0].start : 0;
      const above = pm ? pm.start < firstStart : pin < (items.length ? items[0].index : pin);
      const pinnedEntry = {
        vi: pm != null ? pm : {
          index: pin
        },
        row: rowList[pin],
        pinned: true
      };
      if (above) out.unshift(pinnedEntry);else out.push(pinnedEntry);
    }
  }
  return out;
};

  // Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
  // the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
  // (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
  padTop = () => {
  // SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
  // spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
  // and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
  void this._windowVer.value;
  void this._editVer.value;
  if (!this.rowsWindowed() || !this.virtualizer) return 0;
  const items = this.virtualizer.getVirtualItems();
  let pad = items.length ? items[0].start : 0;
  // D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
  // in-flow as the slice's LEADING <tr> (its measured height is now a real <tr>), so subtract
  // that height from the leading spacer to keep padTop + Σ rendered <tr> + padBottom = total.
  const pin = this.pinnedEditIndex();
  if (pin >= 0) {
    const pm = this.pinMeasurement(pin);
    const inWindow = this.pmIndexInWindow(items, pin);
    if (pm && !inWindow && pm.start < pad) pad = pad - pm.size;
  }
  return pad < 0 ? 0 : pad;
};

  padBottom = () => {
  // subscribe-first, see windowedRows() (IN-04): touch windowVer + editVer before the early
  // return so the fine-grained spacer :style binding subscribes on its first eval + re-derives
  // on pin/unpin.
  void this._windowVer.value;
  void this._editVer.value;
  if (!this.rowsWindowed() || !this.virtualizer) return 0;
  const items = this.virtualizer.getVirtualItems();
  if (!items.length) return 0;
  let pad = this.virtualizer.getTotalSize() - items[items.length - 1].end;
  // D-02 spacer subtraction: when the pinned editing row sits BELOW the window it is rendered
  // in-flow as the slice's TRAILING <tr>, so subtract its height from the trailing spacer.
  const pin = this.pinnedEditIndex();
  if (pin >= 0) {
    const pm = this.pinMeasurement(pin);
    const inWindow = this.pmIndexInWindow(items, pin);
    // WR-01: decide "below the window" by INDEX, not by start-OFFSET. On variable-height rows
    // measurement drift can leave pm.start at-or-past items[0].start while the pinned row's
    // index is actually ABOVE the window, mis-subtracting its height from the trailing spacer.
    // The pinned full-model index vs the last rendered item's index is drift-proof. Fall back to
    // the offset comparison only if the measurement lacks an index (defensive).
    const lastItemIdx = items[items.length - 1].index;
    const below = pm && pm.index != null ? pm.index > lastItemIdx : pm && pm.start >= items[0].start;
    if (pm && !inWindow && below) {
      // below the window → it trailed the slice; subtract its height from the trailing spacer.
      if (pm.end > items[items.length - 1].end) pad = pad - pm.size;
    }
  }
  return pad < 0 ? 0 : pad;
};

  // pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
  pmIndexInWindow = (items: any, idx: any) => {
  for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
  return false;
};

  // rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
  // window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
  rowIsOutsideWindow = (r: any) => {
  if (!this.rowsWindowed() || !this.virtualizer) return false;
  const items = this.virtualizer.getVirtualItems();
  for (const it of items as any) if (it.index === r) return false;
  return true;
};

  // ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
  // rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
  // a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
  // returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
  // cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══
  // windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
  virtualizer: any = null;

  virtualizerCleanup: any = null;

  gridScrollEl: any = null;

  remeasurePending = false;

  // Scroll-end pin state (see recordScrollEnd()): whether the USER left the view at the end,
  // the option count at that moment, and the last scrollTop already accounted for.
  scrollEndPinned = false;

  scrollEndPinnedCount = -1;

  scrollEndPinnedTop = -1;

  // windowSource(): the windowing.rzts host-contract row source — the FILTERED option
  // set. CR-02: the shared windowing contract requires each row to carry a STABLE `.id`
  // (windowing.rzts virtualItemKey reads src[i].id, and the windowed template keys on
  // wr.row.id). A raw Listbox option is a primitive or a bare { label, value, disabled }
  // — NOT guaranteed to have `.id` — so an unwrapped raw set keyed on wr.row.id collapses
  // every framework :key (and every virtual-core measurement key) to `undefined`, which
  // recycles the wrong DOM node as the window scrolls. Wrap each option into an id-bearing
  // row the way the sibling Combobox's filteredOptions() does — `id` is the resolved
  // value, `_opt` the original option (read via wr.row._opt in the windowed template),
  // `_i` the source index. Kept === $data.rows so the math's rowList[vi.index] resolves to
  // the same wrapped row the count windows over.
  //
  // $memo, keyed on the TRUE inputs — NOT on visibleOptions() itself, which returns a
  // FRESH filtered array whenever a query is active (a visibleOptions()-keyed cache
  // would never hit while filtering). The key covers everything the map reads:
  // options ref + query (visibleOptions' inputs) and the optionValue/optionLabel
  // resolvers (valueOf in the map body; labelOf inside visibleOptions' filter path).
  // Same reference-stability contract the sibling Combobox's filteredOptions carries —
  // virtual-core's getItemKey/getMeasurements walk this O(count) per pass, so an
  // unmemoized per-call re-map made every scroll tick O(N²) in wrapper allocations.
  windowSourceCache = {
  keys: null as any[] | null,
  val: null as any
};

  windowSource = () => {
  const __rozieMemoKey = [this.options, this._query.value, this.optionValue, this.optionLabel];
  const __rozieMemoPrev = this.windowSourceCache.keys;
  if (__rozieMemoPrev !== null && __rozieMemoPrev.length === __rozieMemoKey.length && __rozieMemoKey.every((v: any, i: any) => v === __rozieMemoPrev[i])) {
    return this.windowSourceCache.val;
  }
  const __rozieMemoVal = this.visibleOptions().map((o: any, i: any) => ({
    id: this.valueOf$local(o),
    _opt: o,
    _i: i
  }));
  this.windowSourceCache.keys = __rozieMemoKey;
  this.windowSourceCache.val = __rozieMemoVal;
  return __rozieMemoVal;
};

  // D-05 NO-OP PIN HOOK (defined in THIS host, NOT the shared partial — keeps data-table
  // A==B intact). The shared windowedRows/padTop/padBottom call pinnedEditIndex()/
  // pinnedMeasurement() UNGUARDED by convention; a listbox has no edit-pinning, so these
  // reduce the pin union (-1 → never unioned) and the spacer subtraction (null → identity)
  // to a no-op. They MUST exist or the by-convention call ReferenceErrors at mount.
  pinnedEditIndex = () => -1;

  pinnedMeasurement = (pin: any) => null;

  // D-05 windowing.rzts host-contract one-liner (Phase 87 87-02). rowsWindowed() preserves
  // today's EXACT truthiness (byte-behavior-identical) — it is the REQUIRED symbol
  // windowing.rzts calls in place of a bare `$props.virtual` read. NOT type-annotated — this
  // `<script>` block has no `lang="ts"` (unlike windowing.rzts / Combobox.rozie), so the
  // pinMeasurement() explicit-return-type trick (windowing.rzts:65-74) does not apply here; nothing
  // in this plan calls these through a type-narrowing wrapper.
  //
  // GAP-CLOSURE 87-16 (WR-02): the column-axis host-contract symbols (`colVirtualizer`,
  // `colsWindowed()`, `columnCount()`, `columnSize()`, `forcedColumns()`) that 87-02 added
  // alongside this were REMOVED here — they were dead code shipped on a mistaken premise
  // about the compiler's tree-shaking BFS. Listbox imports only `{ virtualItemKey,
  // virtualizerOptions, windowedRows, padTop, padBottom, pmIndexInWindow, rowIsOutsideWindow }`
  // from windowing.rzts; none of those functions' bodies reference the column-axis symbols
  // (only `columnVirtualizerOptions()`/`windowedColIndices()`/`colPadLeft()`/`colPadRight()`/
  // `colIsOutsideWindow()` do, and Listbox never imports any of those), so
  // `inlineScriptPartials()`'s BFS never needed them to exist. See 87-REVIEW.md WR-02 /
  // 87-16-SUMMARY.md for the verification trail.
  rowsWindowed = () => !!this.virtual;

  // autoMeasureOn() (Phase 87 87-07, D-18/D-20): the content-driven-estimate host-contract
  // gate. Listbox never lights this branch — a permanent `false` keeps windowing.rzts's
  // estimateRowSize()/refineRowEstimate() accumulator dead code here. RETAINED (unlike the
  // column-axis symbols above): `virtualizerOptions()` — which Listbox DOES import and call
  // — wires `estimateSize: (i) => estimateRowSize(i)`, and `estimateRowSize()` calls
  // `autoMeasureOn()` as its first line. This one IS reachable through the import graph.
  autoMeasureOn = () => false;

  // Keep $data.rows === windowSource() so the windowing math indexes the live option set.
  syncRows = () => {
  this._rows.value = this.windowSource();
};

  // SCROLL-END PIN (the data-table D-19 twin): keep a user who scrolled to the END of a
  // variable-height list at the end while the options in view measure taller than their
  // estimate. The view is judged on the DOM, and only at a move the USER made — a move is
  // virtual-core's own when it still holds an unreconciled scroll adjustment
  // (scrollAdjustments !== 0): its above-viewport compensation writes an ABSOLUTE scrollTop
  // computed from its last-observed (stale) offset, so it pulls the view back up from the end
  // and must neither clear the pin nor be mistaken for the user leaving the end. That position
  // is remembered so the scroll event that later reports it is not read as a user move either.
  // (Judging on virtual-core's MODEL, as the data-table host does, fails here: its total grows
  // with every option measured in the ResizeObserver batch while its offset stays at the stale
  // value, so the pin was cleared mid-batch — every target ended 10-126px short, measured.)
  recordScrollEnd = () => {
  if (!this.virtualizer || !this.gridScrollEl || this.virtualizer.scrollState) return;
  const top = this.gridScrollEl.scrollTop;
  if (top === this.scrollEndPinnedTop) return;
  this.scrollEndPinnedTop = top;
  if (this.virtualizer.scrollAdjustments !== 0) return;
  // Only a list that actually overflows has an end to hold: while the window has not painted
  // yet (or the list is closed), scrollHeight <= clientHeight reads as "at the end" and a pin
  // recorded then would jump the freshly opened list to the bottom.
  const sh = this.gridScrollEl.scrollHeight;
  const ch = this.gridScrollEl.clientHeight;
  this.scrollEndPinned = ch > 0 && sh - ch > 1 && sh - top - ch <= 1;
  this.scrollEndPinnedCount = this.windowSource().length;
};

  // Re-apply the pin after the framework has committed the window (called from the rAF pass):
  // the real maximum is known only then. Not while a programmatic scroll (scrollToIndex) is in
  // flight, and not when the option count changed since the user reached the end (a new filter
  // or appended options must not be auto-followed).
  keepScrollEnd = () => {
  if (!this.scrollEndPinned || !this.virtualizer || !this.gridScrollEl || this.virtualizer.scrollState) return;
  if (this.windowSource().length !== this.scrollEndPinnedCount) return;
  const maxTop = this.gridScrollEl.scrollHeight - this.gridScrollEl.clientHeight;
  if (maxTop - this.gridScrollEl.scrollTop > 1) {
    this.gridScrollEl.scrollTop = maxTop;
    this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
  }
};

  // Defer remeasureWindow() until AFTER the framework commits the recycled window
  // (onChange fires BEFORE React/Solid commit). TWO deferred passes (microtask THEN rAF)
  // behind one in-flight flag (the data-table virtualization.rzts:46-56 pattern, copied
  // per-consumer per D-04/D-09): the microtask catches Solid's <For> / Svelte's {#each}
  // SYNCHRONOUS commit (the Phase 63 Solid under-convergence hazard — D-09 rAF-defer
  // budget), the rAF catches React's async commit. measureElement is idempotent on an
  // already-observed node, so running both is cheap and loop-free.
  scheduleRemeasure = () => {
  this.recordScrollEnd();
  if (this.remeasurePending) return;
  this.remeasurePending = true;
  let ranMicro = false;
  const microPass = () => {
    this.remeasureWindow();
  };
  // N-05 (quick 260923-rrr): key the rAF pass on the OUTCOME. React and Angular commit the
  // recycled window AFTER the first rAF, so one pass measured the OLD options and the new ones
  // waited for virtual-core's 150ms scrolling-ended tick — with variable-height options the late
  // above-viewport adjustment then moved the whole list (measured). Re-run next frame until the
  // committed options cover the virtualizer's window, bounded (the data-table host twin).
  let rafAttempts = 0;
  const rafPass = () => {
    const covered = this.remeasureWindow();
    rafAttempts = rafAttempts + 1;
    if (!covered && rafAttempts < 10 && typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(rafPass);
      return;
    }
    this.keepScrollEnd();
    this.remeasurePending = false;
  };
  if (typeof queueMicrotask !== 'undefined') {
    ranMicro = true;
    queueMicrotask(microPass);
  }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) this.remeasurePending = false;else setTimeout(rafPass, 0);
};

  // measureElement sweep: hand every rendered windowed option to the virtualizer so its
  // true (variable) height is observed (virtual-core measures ONLY nodes passed to
  // measureElement, keyed by the data-index attribute). Bails during a programmatic
  // scroll (scrollToIndex) so a measure can't starve the scroll target.
  remeasureWindow = () => {
  if (!this.virtualizer || !this.gridScrollEl) return true;
  if (this.virtualizer.scrollState) return true;
  const els = this.gridScrollEl.querySelectorAll('.rozie-listbox-option[data-index]');
  const rendered = new Set();
  for (const el of els as any) {
    this.virtualizer.measureElement(el);
    rendered.add(el.getAttribute('data-index'));
  }
  // N-05: false while the framework has not yet committed the recycled window.
  const items = this.virtualizer.getVirtualItems();
  for (let i = 0; i < items.length; i++) {
    if (!rendered.has(String(items[i].index))) return false;
  }
  return true;
};

  // ---- focus / scroll helpers (post-mount $refs only) --------------------
  // Impure ($refs) → per the ROZ123 + A==B rules they stay in the host (the spine
  // only closes over them). Named `focusControl` (not `focus`): a `focus` $expose
  // verb would override the inherited HTMLElement.focus method on the Lit element.
  focusControl = () => {
  this._refTriggerEl?.focus();
};

  // Keep the active option visible inside the scrolling listbox. Reads $refs in
  // a post-mount callback only (never eagerly — ROZ123). When windowing, route through
  // the virtualizer (scrollToIndex) so an active option OUTSIDE the rendered window is
  // scrolled into view (the windowed-arrow-nav seam); else the native scrollIntoView.
  scrollActiveIntoView = () => {
  if (this._activeIndex.value < 0) return;
  if (this.virtual && this.virtualizer) {
    // 'center' (not 'auto'): keep the active option well inside the rendered slice as the
    // window scrolls — 'auto' lands it at the viewport edge where the overscan band can
    // leave it just-unrendered for a frame on the fine-grained targets (Solid).
    this.virtualizer.scrollToIndex(this._activeIndex.value, {
      align: 'center'
    });
    this.scheduleRemeasure();
    return;
  }
  if (!this._refListEl) return;
  const el = this._refListEl.querySelector('#' + CSS.escape(this.optionId(this._activeIndex.value)));
  el?.scrollIntoView({
    block: 'nearest'
  });
};

  // ---- windowing lifecycle (post-mount; ONLY when virtual) ----------------
  // kickWindow: the cross-target first-paint settle. Re-captures the LIVE scroll element,
  // re-feeds the CURRENT option count into the virtualizer, re-attaches its rect observer
  // (_willUpdate), and bumps the windowVer signal so the windowed <For>/{#each}/repeat
  // re-derives. Retried over a few frames because (a) virtual-core measures the scroll rect
  // asynchronously (D-09 Solid rAF-defer — a synchronous kick sees rectH 0 → empty window),
  // (b) Solid/Lit recreate the list node between mount and first commit (leaving virtual-core's
  // scrollElement stale), and (c) the consumer often seeds options AFTER the listbox mounts
  // (Lit/React), so the count must be re-read once the prop propagates. Stops once the window
  // paints (or attempts run out) — idempotent + loop-free.
  kickWindow = (attempts: any) => {
  if (!this.virtualizer) return;
  this.gridScrollEl = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rozie-listbox-list') : this.gridScrollEl;
  // Only re-feed the count from a NON-EMPTY source: on React these rAF closures capture
  // stale (mount-time, empty) props, so feeding here would CLOBBER the $watch's correct
  // count back to 0. The $watch (fresh useEffect props) owns React's count; the kick owns
  // the Solid/Lit scroll-element re-attach + the deferred windowVer re-derive.
  if (this.windowSource().length > 0) {
    this.syncRows();
    this.virtualizer.setOptions(this.virtualizerOptions());
  }
  this.virtualizer._willUpdate();
  this._windowVer.value = this._windowVer.value + 1;
  this.remeasureWindow();
  if (this.windowedRows().length === 0 && attempts > 0) {
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => this.kickWindow(attempts - 1));else setTimeout(() => this.kickWindow(attempts - 1), 16);
  }
};

  // nextAutoId(): a page-wide counter shared by every Rozie component instance. It
  // lives on globalThis (read through Reflect, which type-checks in the plain-JS and
  // the TS script alike) so separately bundled copies of a leaf never hand out the
  // same id. The same four lines live in Combobox, Listbox and Popover.
  nextAutoId = () => {
  const n = (Number(Reflect.get(globalThis, '__rozieAutoId')) || 0) + 1;
  Reflect.set(globalThis, '__rozieAutoId', n);
  return n;
};

  // idRoot(): the `id` prop, else the per-instance id generated in $onMount, else the
  // pre-mount fallback. Also the listCore.rzts host contract (its optionId reads it).
  idRoot = () => this.id || this._autoId.value || 'rozie-listbox';

  get value(): unknown { return this._valueControllable.read(); }
  set value(v: unknown) { this._valueControllable.notifyPropertyWrite(v); }

  /**
   * Plan 14-05 — cross-framework attribute fallthrough source. Reads the
   * host custom element's attributes on each call so a consumer-side bound
   * attribute flows through on every render. The `rozieSpread` directive
   * (D-02) does the cross-render diff downstream.
   *
   * Phase 15 follow-up Bug A — declared-prop attribute names are filtered
   * out so `$attrs` returns "rest after declared props" (semantic parity
   * with React/Vue/Svelte/Solid/Angular). Both Lit attribute-naming
   * forms are folded into the skip set: kebab-case for model props
   * (explicit `attribute:`) AND lowercased property name (Lit's default).
   *
   * command-palette-per-level-virtual / portal-through-portal cluster —
   * `data-rozie-ref` is ALWAYS skipped too (a reserved compiler bookkeeping
   * attribute, never a consumer prop) so a parent-assigned `ref=` on this
   * component's own host tag can never clobber this component's OWN
   * internal `data-rozie-ref` ref markers via fallthrough re-application.
   */
  private get $attrs(): Record<string, string> {
    const __skip = new Set<string>(['data-rozie-ref', 'options', 'value', 'multiple', 'inline', 'disabled', 'placeholder', 'close-on-select', 'closeonselect', 'option-label', 'optionlabel', 'option-value', 'optionvalue', 'option-disabled', 'optiondisabled', 'id', 'aria-label', 'arialabel', 'virtual', 'estimate-row-height', 'estimaterowheight', 'max-height', 'maxheight']);
    const out: Record<string, string> = {};
    for (const a of Array.from(this.attributes)) {
      if (__skip.has(a.name)) continue;
      out[a.name] = a.value;
    }
    return out;
  }

  /**
   * Phase 15 D-19 — consumer-passed listener cluster placeholder.
   * Lit attaches event listeners directly on the host element via
   * `addEventListener` (no per-instance prop rest binding), so the
   * runtime value is undefined; the `rozieListeners` directive's
   * nullish coercion (`obj ?? {}`) handles the no-op cleanly.
   * The declaration exists to satisfy `tsc --noEmit` on consumer
   * projects with strict mode — bare `$listeners` in `render()`
   * would otherwise raise TS2304 (Cannot find name).
   */
  private get $listeners(): Record<string, EventListener> | undefined {
    return undefined;
  }
}

Each is a real component for its framework — React forwardRef + hooks, Vue <script setup> + defineModel, Svelte 5 runes, an Angular standalone component (with ControlValueAccessor), a Solid component, and a Lit custom element. Same props, same events, same two-way value, same scoped slots, same imperative handle — identical on every target, with no third-party engine behind it.

See also ​

Pre-1.0 — APIs may change between minor versions.