---
id: "packages/active-virtual/changelog"
title: "Changelog"
description: "What changed for users of active-virtual, per version."
url: "/packages/active-virtual/changelog"
locale: "en"
revision: "dd69bb911e110ecb"
---

# Changelog

> [!NOTE]
> **Release state**
>
> The version line at the top of this page is read from the package and the
> release register of Foundation. A recorded release counts only for exactly the
> content it recorded: when the package changed after it, this site does not
> call that version released. The section Unreleased lists what changed since,
> including the changes made since Foundation maintains the package.

Notable changes to `@workspace/active-virtual`. Format based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning per
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

This file is the authoritative release history for `@workspace/active-virtual`.
The pre-1.0.0 entries (v0.1.0 → v0.2.8) document the internal iteration that
led to the v1.0.0 externally-publishable baseline and are preserved verbatim
in intent (internal tracking IDs and test-count tallies have been stripped —
the behavior change is what stays).

---

## \[Unreleased\]

### Maintained in Foundation (since 2026-10-21)

- **Documentation:** references to documents of the source repository are
  text, marked `(source repository: …)`, instead of links that resolve nowhere here. No
  code changed.

## \[2.1.0\] — 2026-10-18

### Changed — canonical transfer form (2026-10-18, platform ADR-112)

- **The manifest is the transferable manifest** (RPS-D7): no workspace
  package in `devDependencies` (what the package's own development takes from
  Datyx, such as the lint preset, is host tooling in the reusable package
  register); the declaration packages of what the source imports sit in the
  field of their module (`@types/react` and `@types/react-dom` as optional
  peers beside the `react` peers). Nothing changes for a Datyx consumer.
- **`tsconfig.json` is self-contained**: it no longer extends
  `@workspace/typescript-config`, with the same compiler options, because a
  bundler in a consumer reads it for every source file it compiles.

### Added (portable runtime P6, D-12)

- `toLogical`, `toPhysical` and `ScrollPlane` are exported from the root
  entry: the one conversion between a physical scroll offset and the logical
  offset, for a consumer that reads or writes `scrollLeft` beside an axis.
  Unchanged behaviour.

### Fixed

- **A request whose write landed short before the layout caught up is
  written again.** When an instant write of a request to an item or an edge
  lands short because the container had not laid out a grown extent yet, the
  browser adapter writes the same target once more as soon as the
  container's scroll range has grown, checked at that request's own ticks.
  Only the same, still active request with the same target is served, only
  on growth and never because time passed; the write and visible-time
  budgets run on unchanged, and a target that stays out of reach gives up as
  before (`DECISIONS.md` D-13). No API change.
- **An instant scroll write the browser clamps no longer leaves the axis at
  the requested offset.** After every instant write the browser adapter gives
  the axis the offset the browser actually reached, also when the write moved
  nothing and no scroll event follows (a container already at its physical
  end, or with no scroll range). The projection now shows what the viewport
  shows, and such a request never settles on the offset it asked for: it keeps
  its target, warns `scroll-unreachable`, and gives up at its budget.
- **A request's pending write no longer lands after the request has ended.**
  A request that is superseded, interrupted, loses its target or gives up
  before its write reaches the browser now writes nothing, also when the newer
  request writes nothing itself (for example one for a missing key). A
  compensation the withdrawn write had replaced still lands, and the other
  axis's writes are untouched; the axis returns to the offset the container
  keeps.
- **Measurements held back during a smooth request no longer regain withdrawn
  authority.** A held measurement is withdrawn by a newer accepted measurement
  of the item, an invalidation of it (`stale` or `void`), a reset, or the item
  leaving the collection. It no longer returns after a `void` invalidation,
  never applies to a key that was removed and inserted again, and never
  overwrites a newer measurement.
- **A running smooth scroll animation no longer outlives its request.** When
  a newer request supersedes a smooth request whose animation the browser is
  still running, the browser adapter stops that animation, also when the
  newer request writes nothing (a missing item, or `auto` to an item already
  visible); the viewport stays at the offset the newer request was resolved
  against instead of drifting to the old target. The same holds when a
  collection change removes the request's target and when input interrupts
  it, including in Firefox, which kept animating through a write to the
  current position. Only the ended request's direction stops: the other
  axis's smooth request carries on and lands. An animation whose request
  settles or gives up, or that the user takes over by moving the container,
  is left alone.

### Documentation

- The ROADMAP holds no open package work. What stood there as open choices
  is recorded as current contract or envelope (`DECISIONS.md` D-14): an
  active request across a container switch (now also pinned by a test), the
  2 000-unit visible-time budget for long smooth scrolls, and the O(n) key
  index for keyed edits. Evidence and decisions owned elsewhere moved to
  active-grid: the Safari RTL measurement (R-61) and the legal question
  about versions before 2.0.0 (TD-42).
- `Projection.visible` is described as it behaves: with one lane every
  position between its bounds intersects the viewport, except that a viewport
  edge in a gap, a padding or beyond the content takes in the nearest item on
  that side. `API.md` and the TSDoc said every position between them was
  visible, which a layout `gap` contradicted. No runtime change; the rule is
  now pinned in `range.test.ts`.
- The 1.x → 2.0.0 migration table moved from `API.md` into the 2.0.0 entry
  below: `API.md` carries the current contract only.
- `ARCHITECTURE.md` states the actual cost of a scroll: every offset change
  rebuilds the rendered items and compares the projection by value, O(w) on
  top of the lane searches, and sorts the merged items when there is more than
  one lane. A key sequence rebuilds the key index and the size register
  directly (O(n)); edits are derived only to follow a removed anchor. `API.md`
  no longer says the engine derives the edits of a key sequence. No runtime
  change.
- `API.md` states the exact rules for held measurements, a reused key, a
  withdrawn pending write and a write that lands short, instead of the
  absolute "never dropped" and "never counts as reached" claims.
- `THIRD-PARTY-NOTICES.md` is removed. It carried the TanStack Virtual (MIT)
  notice for the engine of the versions before 2.0.0; that engine and all of
  its source were removed in 2.0.0, and a provenance review of the shipping
  engine supported its independence (`docs/DECISIONS.md` D-8). This is not a
  legal opinion.
- The build-time documents of the independent rewrite (product contract,
  programme architecture, acceptance baseline, research) are removed; their
  durable content is in `API.md`, `ARCHITECTURE.md`,
  `FUNCTIONEEL-OVERZICHT.md` and `DECISIONS.md`. `API.md` now states the
  guarantees and measured limits per browser itself.

## \[2.0.0\] — 2026-09-25

### Breaking — a new engine and API

- **The engine is replaced.** The package ships one, independently written
  engine: a framework-neutral, DOM-independent core per axis, a browser scroll
  host, and a React adapter. The previous engine, its React hooks and its
  whole API are removed; nothing of the 1.x API remains.
- **Root entry:** `createAxisStore`, `createScrollHost`, `findScrollHost` and
  the value types (`AxisInit`, `AxisInput`, `Projection`, `ProjectedItem`,
  `AnchorPolicy`, `RangePolicy`, `Diagnostic`, …).
- **`./react`:** `useVirtualAxis`, one hook per axis, returning the projection
  of the render, a controller with a stable identity, `itemRef` and
  `isRendered`; `inspectAxis` for lifecycle tests.

### Added

- Keyed identity with explicit edits and key sequences; measurements follow
  their item.
- Anchoring through every geometry change (start edge, end edge, keyed,
  focus, none, or a function per change) and edge follow for end-oriented
  lists; a removed anchor is replaced by its nearest surviving neighbour.
- Explicit invalidation (`stale`, `void`) and reset with collection epochs;
  a measurement for an old epoch or an unknown item is dropped.
- Range observation (`observeRange`, `onRangeChange`) and `isRendered`.
- Focus custody in the React adapter (opt-in).
- An explicit pre-measurement status (`awaiting-viewport`) that is identical
  on the server and the first client render; seeded server windows.
- A browser adapter that merges the scroll writes of one turn per container,
  recognises its own writes, and reports a write the browser cannot reach.

### Changed

- Scroll requests and compensations are clamped to the declared extent;
  content that follows the list inside the container is declared as
  `paddingEnd`, content before it as `listOrigin`.
- A measurement equal to an item's current size is kept and survives a later
  estimate change.
- Diagnostics are structured (`code`, `message`, `data`) and go to one sink.

### Migration

Replace `useVirtualizer` / `useWindowVirtualizer` with `useVirtualAxis`,
`estimateSize` with `estimate`, `getVirtualItems()` / `getTotalSize()` with
`projection.items` / `projection.extent`, `ref={measureElement}` plus
`data-index` with `ref={itemRef(identity)}`, `measure()` with
`controller.reset()`, and `scrollToIndex` with `controller.scrollToPosition`.
The full table:

| 1.x | 2.0.0 |
| --- | --- |
| `useVirtualizer({count, getScrollElement, estimateSize})` | `useVirtualAxis({count, container, estimate})` |
| `horizontal: true` | `orientation: 'horizontal'` |
| `getItemKey` | `keys` (keyed identity) instead of `count` |
| `getVirtualItems()`, `getTotalSize()` | `projection.items`, `projection.extent` |
| `VirtualItem.index`, `.start` | `ProjectedItem.position`, `.offset` |
| `ref={measureElement}` with `data-index` | `ref={itemRef(identity)}`; no attribute |
| `measure()` | `controller.reset()`, or `controller.invalidate(target, strength)` |
| `scrollToIndex(i, {align})` | `controller.scrollToPosition(i, {align})` |
| `onWarn`, `onDebug` | `onDiagnostic` |
| `useWindowVirtualizer` | `useVirtualAxis({container: window, …})` |
| `shouldAdjustScrollPositionOnItemSizeChange` | `anchor` (a choice, or a function of the geometry change) |
| `rangeExtractor` | `rangePolicy` |
| `initialRect`, `initialOffset`, `initialMeasurementsCache` | `seed: {viewportExtent, offset, sizes}` |
| `new Virtualizer({...})` with observers | `createAxisStore(init)` and `createScrollHost(container).attach(axis, options)` |
| Clamped to the element's physical scroll range | Clamped to the declared extent; declare trailing content as `paddingEnd` (D-6) |

## Unreleased changes to the replaced engine (superseded by 2.0.0)

These entries describe the 1.x engine after 1.0.1; they were never released
on their own, and 2.0.0 removed that engine.

### Fixed — standards convergence

- `getVirtualItemForOffset` returns an item that covers the offset when lanes
  have different heights: starts do not rise with the index across lanes, so
  each lane is searched on its own (the lowest covering index wins; in a gap,
  the nearest item below). A single lane is unchanged.
- `getTotalSize()` of an empty list keeps `paddingStart` and `paddingEnd` when
  a `scrollMargin` is set (it returned `0`).

### Documentation — standards convergence

- The range contract says precisely that in one lane the first and last item
  of `calculateRange()` may lie just outside the viewport when the offset falls
  in a `gap` or past the last item. Scale and browser claims narrowed to what is
  measured (operation counts up to 100,000 items; one recorded T6 run).

### Documentation

- **License and attribution.** active-virtual is proprietary and
  workspace-private (`LICENSE`, `UNLICENSED`). The MIT notice of TanStack
  Virtual, from which it is derived, is now reproduced in
  `THIRD-PARTY-NOTICES.md`; README and docs no longer call the package MIT.
- The documentation describes the current engine and the current public API,
  and no longer carries the v1.0.0 external-publication promise: the package is
  not published, and its API is what the two entry points export.

### Fixed

- **Measurements are recomputed incrementally.** A read recomputes only from the
  first item whose measurement became invalid; a read with nothing invalid does
  no work. Previously every internal read rebuilt every item, so a scroll event
  cost O(count) (about 5 ms per event at 100 000 items) and measuring a mounted
  window cost O(count) per measured item. Unchanged reads no longer depend on
  the item count; neither do scroll events, in one lane or in several (see
  below for strongly uneven lanes).
- **Zero-size items no longer drop out of the rendered range.** Items of size 0
  that share a start position with their neighbours stay in the range, so they
  are measured again once they have a size.
- **A pre-mount render no longer stays on screen when mounting empties the
  range.** Render-skipping compares against the index set the subscriber last
  read, not the set last notified.
- **The total size frozen during a scroll follows a change of `count`.**
- **A smooth `scrollToIndex` no longer interrupts itself.** Every scroll event
  of the animation between the previous position and the target belongs to the
  programmatic scroll; only a movement away from the target, or past it, is a
  user interruption. Reconciliation waits while a smooth scroll progresses
  instead of re-issuing it every other frame, so deferred measurements are
  applied on arrival rather than after the second frame.
- **Cleanup is a hard boundary.** The scroll-end timer and the animation frames
  of the container and item observers are cancelled on cleanup and on a
  scroll-element swap, and callbacks of an older subscription are ignored, so no
  late work writes into an unmounted or re-subscribed instance. The container
  observer's frame now runs on the owning window (iframe-aware).
- **Cleanup no longer constructs a `ResizeObserver`.** Releasing an element or
  disconnecting when no item observer exists does nothing.
- **Options changed through `setOptions` take effect on the mounted element.**
  `useAnimationFrameWithResizeObserver` is read at every resize;
  `isScrollingResetDelay` and `useScrollendEvent` re-subscribe the offset
  observer at the next `_willUpdate()` without replacing the scroll element.
- **Horizontal RTL scrolling writes as it reads.** `scrollToOffset`, `scrollBy`,
  `scrollToIndex` and resize corrections now use the same logical offset as the
  offset observer; the built-in element and window observers map it to the
  negative RTL `scrollLeft`/`scrollX` of CSSOM View.
- **Changing `horizontal` no longer keeps sizes measured along the old axis.**
  The change drops the old-axis sizes and scroll state at once; the next
  `_willUpdate()` re-subscribes, writes `initialOffset` along the new axis and
  measures the mounted items again.
- **A page hidden during `scrollToIndex` no longer aborts it on return.** Time
  the owning document is hidden no longer counts toward the reconciliation
  timeout, so the scroll continues instead of ending with a spurious timeout
  warning and an early flush of deferred measurements.
- **The latest scroll command wins.** `scrollToOffset` and `scrollBy` now end
  a pending `scrollToIndex` reconciliation, as a newer `scrollToIndex` already
  did, so an older reconciliation no longer drags the list back to its index.
  A user scroll that the running `scrollToIndex` cannot explain now ends it in
  instant mode too, not only in smooth mode.
- **A window from another realm is virtualized.** The owner window of a
  window scroll element is resolved without `instanceof Window`, so an
  iframe's `contentWindow` gets its scroll listener, timers and frames.
- **A measurement replaces an `initialMeasurementsCache` seed for good.** The
  seeds are applied once, before the first computation. Previously an
  invalidation from index 0 (a measurement of item 0, or any structural option
  change) applied them again and overwrote newer measured sizes. `measure()`
  and `enabled: false`, which discard every measurement, still start from the
  seeds again.
- **The newest scroll command is the only writer, also under re-entry.** A
  newer command, a cleanup or a scroll-element swap issued synchronously
  inside a `scrollToIndex`'s own write (a custom `scrollToFn` that reports the
  offset at once) or inside `estimateSize`/`getItemKey` while it computes its
  target now ends that `scrollToIndex` at once. Previously its reconciliation
  scheduled one more verification and wrote its old target over the newer
  command.
- **A command issued from any callback of an older command wins.** Every
  command now owns the scroll target before it ends the previous one, and
  checks its ownership after each consumer callback. A newer command issued
  from `onChange` (while deferred measurements of the previous command apply),
  from the smooth-scroll `onWarn` of `scrollToOffset`/`scrollBy`, from
  `estimateSize` in a later `scrollToIndex` verification, or from
  `onDebug('Schedule retry')` is no longer overwritten by the older command.
  Previously the older command still wrote its target, a later verification
  threw on its cancelled state, or a retry scheduled an obsolete frame. The
  smooth-scroll warning of `scrollToOffset`/`scrollBy` now follows the end of
  the previous command.
- **Applying deferred measurements stops when ownership passes on.** A batch of
  measurements a smooth `scrollToIndex` deferred is the work of the command
  that flushes it, and its own callbacks reach consumer code: `measureElement`,
  `shouldAdjustScrollPositionOnItemSizeChange`, the `correction` debug
  notification, `onChange`, and `estimateSize`/`getItemKey` while a later entry
  is looked up. A command issued from any of them now takes the scroll target
  over at once and the rest of the batch is dropped — no further correction, no
  size recorded, no notification — at a command's start, at a user interruption
  and at the settlement of a reconciliation alike. Previously the batch ran on:
  a two-entry flush wrote `[230, 3000, 260]` where the newer command's 3000 had
  to be last, a settled reconciliation wrote `[5030, 3000, 5060]`, and a single
  entry whose own callback replaced the owner still wrote its stale correction
  after it (`[3000, 230]`). Measurement that belongs to no command
  (`measureElement()` from a ref, `resizeItem()`, `measure()`) is unchanged.
- **`align: 'end'` aligns the requested item in a multi-lane layout.** The
  scroll-to-the-very-end rule (maximum offset, `paddingEnd` in view) now
  applies to the item whose end is the end of the content, not to the last
  index; in several lanes an earlier item can reach further, and the last
  index then aligns by its own end. The result is also clamped at 0 when the
  content is shorter than the viewport.
- **The frozen total size follows every layout change.** During a scroll
  episode `getTotalSize()` was re-based only when `count` changed. It is now
  re-based on any structural change: `gap`, `paddingStart`, `paddingEnd`,
  `scrollMargin`, `lanes`, `horizontal`, `enabled`, a new `estimateSize` or
  `getItemKey` function (an active-grid column resize mid-scroll), or
  `measure()`. Measured size changes stay frozen as before.
- **Multi-lane totals and placement no longer scan the list.** An unchanged
  `getTotalSize()` read one item per lane at best but, in strongly uneven
  lanes, every item back to the start (100 000 inspections per read at
  100 000 items); placing an item and resuming after an invalidation scanned
  backward the same way, so building such a layout was quadratic. Per-lane
  bookkeeping now makes an unchanged read one inspection per lane, an append a
  handful, and a recomputation the changed suffix plus the affected lanes.

### Changed

- **Breaking: visibility is per lane, and the engine applies the overscan.**
  With `lanes > 1` every lane is searched in its own geometry, and the rendered
  set is each lane's visible run plus its own directional overscan, merged in
  ascending index order. It need not be contiguous by index: a strongly uneven
  (masonry) layout renders only the visible items of each lane and their
  overscan, however far apart their indexes are, at O(lanes × log count) plus
  the visible items per scroll event. Previously one index interval covered
  every index between the lowest and highest visible item and was expanded
  until every lane passed the viewport, which in the extreme rendered the
  whole list at O(count) per scroll event. `overscan` now counts items per
  lane (roughly rows) instead of indexes. One lane renders exactly what it
  rendered before.
- **Breaking: `Range` gains `indexes`**, the default render set (visible items
  plus per-lane overscan, ascending). `startIndex` and `endIndex` are the
  lowest and highest visible index; in several lanes they are bounds only, as
  is `calculateRange()`. The default `rangeExtractor` returns `range.indexes`;
  a custom extractor extends that set instead of computing overscan.
- A new identity of `estimateSize` or `getItemKey` is now the signal to
  recompute every item; a function that keeps its identity is treated as pure.
  Keep both stable and pass a new function when estimates or keys change.
- Measured sizes of keys the list no longer produces are dropped once the size
  cache holds more sizes than there are items.
- **Size and count contract.** `count` must be a non-negative integer, or the
  constructor and `setOptions` throw `RangeError`. An `estimateSize` result must
  be finite and `>= 0`, or the read that needs it throws `RangeError`. An invalid
  measured size (`measureElement`, `resizeItem`) is reported through `onWarn`
  and ignored. Zero and fractional sizes stay valid.
- **Layout option contract.** `lanes` must be an integer `>= 1`; `gap`,
  `paddingStart`, `paddingEnd`, `scrollPaddingStart`, `scrollPaddingEnd` and
  `scrollMargin` must be finite and `>= 0`. An invalid value throws
  `RangeError` from the constructor or `setOptions`, and a rejected
  `setOptions` keeps the previous options.
- **`initialMeasurementsCache` sizes obey the size contract.** A seeded size
  that is negative, `NaN` or infinite throws `RangeError` when the cache is
  seeded. Only `key` and `size` are read from the cache.
- Changing `isScrollingResetDelay` or `useScrollendEvent` during a scroll ends
  the running scroll episode (`isScrolling` becomes `false`).
- **Explicit public surface.** Both entry points name every export; nothing is
  re-exported by wildcard. A contract test pins the export lists (types
  included), the export map and the public members of `Virtualizer`.
- `./react` exports `Virtualizer` as a type only. Construct a `Virtualizer`
  from the root entry.
- **Read-only shared shapes** (type level, no runtime freeze). The fields of
  `VirtualItem` and `Range` are `readonly`; `getVirtualItems()` returns
  `readonly VirtualItem[]` and `getVirtualIndexes()` `readonly number[]`,
  because both arrays are memoized and the items are the virtualizer's cached
  measurements. `rangeExtractor` may return a `readonly number[]`,
  `initialMeasurementsCache` accepts a `readonly VirtualItem[]`, and the members
  of `VirtualizerLike` are `readonly`. Code that mutated a returned item or
  array no longer compiles.
- `calculateRange`, `getVirtualIndexes` and `getVirtualItems` are typed as
  plain functions; the internal memo helper's `updateDeps` is no longer part of
  their type.
- `elementScroll`, `windowScroll`, `measureElement` and the four observers are
  generic over the item element type instead of accepting `any` there, so they
  are assignable to any `VirtualizerOptions` without a cast.

### Added

- `VirtualizerLike` is exported from the root entry: the instance as the
  pluggable functions (`scrollToFn`, the observers, `measureElement`,
  `onChange`) receive it, for typing a custom one.
- `./react` exports `UseVirtualizerOptions` and `UseWindowVirtualizerOptions`,
  the option types of the two hooks, and `AdaptiveOverscanOptions`.

### Removed

- **Breaking:** root exports that were implementation helpers, public only
  through wildcard re-exports: `approxEqual`, `debounce`, `defaultKeyExtractor`,
  `findNearestBinarySearch`, `getElementRect`, `indexesEqual`, `memo`,
  `MemoizedFunction`, `MemoOptions`, `notUndefined`, `resolveOverscan`,
  `PartialKeys` and `ScrollToOptions` (still the shape behind
  `ScrollToIndexOptions` and `ScrollToOffsetOptions`). `debounce` itself is
  deleted: nothing called it since the scroll-end timer became cancellable.
- **Breaking:** `./react` no longer exports `ReactVirtualizerOptions`, an alias of
  `VirtualizerOptions` (use `VirtualizerOptions` or the hook option types), or
  `PartialKeys`.
- **Breaking:** `defaultRangeExtractor`. The default render set, overscan
  included, is `Range.indexes`; a custom `rangeExtractor` extends it.
- **Breaking:** the `Virtualizer` instance members `range` (use
  `calculateRange()`) and `elementsCache` (internal bookkeeping of the measured
  elements).

---

## \[1.0.1\] — 2026-05-20

**Internal type-cleanup.** Reduces `@typescript-eslint/no-explicit-any` warnings from 14 → 0 across the package via two centralized type-aliases and a handful of motivated `eslint-disable` directives. No API surface changes; no runtime behaviour changes.

### Changed (internal)

- **New internal `ScrollObserverInstance<T>` and `ItemMeasurementInstance<T>`** type-aliases in `core/virtualizer/types.ts`. They consolidate the `VirtualizerLike<T, any>` / `VirtualizerLike<any, T>` pattern used by observer-functions where one of the two generic parameters is irrelevant to the caller. The `any` in the unused slot is required because `VirtualizerLike` is invariant in both parameters (`TItemElement` appears in parameter and return positions of `options.measureElement`/`options.onChange`), so narrowing to `Element` would reject valid downstream-typed instances. Motivated `eslint-disable` lives on the alias-definitions with JSDoc rationale.
- **`core/observers/element-observers.ts`** uses `ScrollObserverInstance<T>` for `observeElementRect`, `observeElementOffset`, and `elementScroll`.
- **`core/observers/window-observers.ts`** uses `ScrollObserverInstance<Window>` / `ScrollObserverInstance<T>` for `observeWindowRect`, `observeWindowOffset`, and `windowScroll`.
- **`core/observers/measure-element.ts`** uses `ItemMeasurementInstance<TItemElement>` for `measureElement`.
- **`core/utils/memo.ts`** introduces a file-internal `AnyDepsTuple = ReadonlyArray<any>` alias for variadic-tuple-inference on `TDeps` (TS #28259). Single `eslint-disable` on the alias-definition covers all three call-sites (`MemoOptions`, `MemoizedFunction`, `memo` function-generic). Internal storage variable `deps` switched from `ReadonlyArray<any>` → `ReadonlyArray<unknown>` (the storage role doesn't need the variadic-tuple semantics). Removed explicit `: any` annotation from the `.some()` callback parameter (TypeScript infers from `TDeps`).
- **`core/utils/helpers.ts`** `debounce` retains its variadic-tuple-inference pattern (`TArgs extends Array<any>`) and transparent `this`-forwarding (`this: any`), now with motivated `eslint-disable` directives explaining each.

### Not changed

- Public API surface: no changes to `Virtualizer`, observer functions, `measureElement`, `memo`, or any exported type/function shape.
- Runtime behaviour: no changes.
- Downstream `@workspace/active-grid` consumer still typechecks clean.

### Tests

- All 221 tests remain green (no behaviour changes, no test additions).

---

## \[1.0.0\] — 2026-05-19

**First stable major release.** Promotes the 0.2.8 surface to a stable,
externally-publishable baseline with an API-stability promise from this
version onward.

No API changes versus 0.2.8 — this release is the version-promotion itself.

### Notes for consumers

- Import paths, surface, and runtime behaviour are unchanged from 0.2.8.
- The README license block has been corrected to match the `LICENSE` file
  (MIT — TanStack-Virtual-derivation). The previous "All rights reserved"
  wording was misleading documentation, not a license change.

  - *Correction, 2026-09-19:* this no longer describes the package. Its
    `LICENSE` is the proprietary Datyx license and `package.json` says
    `UNLICENSED`; active-virtual is not MIT-licensed. The MIT notice of
    TanStack Virtual, from which the versions before 2.0.0 were derived, was
    carried in `THIRD-PARTY-NOTICES.md` until that engine was removed. The
    entry above is kept as it was released.
- The `src/index.ts` packageDocumentation now states "React 19 compatible"
  only, matching `package.json` peerDeps (`^19.0.0`) and the README claim.
- Full package documentation now lives under `docs/`; this `CHANGELOG.md`
  and `DECISIONS.md` moved from the package root into `docs/` as part of
  the same restructure.

---

## \[0.2.8\] — 2026-04-06

### Fixed

- **`measure()` clears deferred measurements and cancels smooth scroll.**
  `measure()` now clears `_deferredMeasurements`, cancels the active
  `_cancelScrollToIndex`, and resets `_smoothScrollState`. Previously,
  a `measure()` call during an active smooth scroll (triggered by a
  cross-package sort/filter flow in `active-grid`) could leave stale
  deferred measurements that would be applied after the reset.

---

## \[0.2.7\] — 2026-04-06

### Fixed

- **EMA velocity no upper bound.** Added `MAX_VELOCITY` cap
  (10 000 px/s) after the EMA update in `_updateScrollVelocity()`.
  macOS momentum scrolling can reach 50k–100k px/s, which would produce
  thousands of overscan rows. The cap prevents extreme overscan while
  preserving smooth velocity tracking for normal scroll speeds.

---

## \[0.2.6\] — 2026-04-06

### Fixed

- **EMA velocity one-frame blank after scroll pause.** Velocity tracking
  no longer hard-resets `_lastScrollOffset`, `_lastScrollTime`, and
  `_scrollVelocity` to 0 when scrolling stops. Instead, tracking state
  is preserved and velocity decays gradually via `Math.pow(0.5, dt / 100)`
  when the next scroll event arrives after a pause > 100 ms. Prevents a
  one-frame blank flash (overscan = 0) when rapidly resuming scroll after
  a brief pause.
- **User scroll interrupt now cancels reconciliation.** When a
  user-initiated scroll is detected during a smooth `scrollToIndex`
  animation, the reconciliation RAF loop is now cancelled
  (`_cancelScrollToIndex`) in addition to flushing deferred measurements.
  Previously, the RAF loop continued running after interrupt.

---

## \[0.2.5\] — 2026-04-06

### Fixed

- **scrollToIndex cancel/new race condition.** `scrollToIndex()` now
  calls `_flushDeferredMeasurements()` before cancelling the previous
  reconciliation and setting new smooth scroll state. Previously,
  deferred measurements from the previous scroll were lost because
  `_smoothScrollState` was overwritten and `_deferredMeasurements`
  cleared before `cancel()` was called. The `cancel()` function skips
  `onSettled`, so deferred measurements were never flushed.

---

## \[0.2.4\] — 2026-04-05

### Tests

- **Explicit `cancel()` unmount behavior.** Test verifies that
  `cancel()` prevents `scrollToFn` from being called after the
  virtualizer is cleaned up. Dynamic mode with changing offsets — after
  `cancel()`, no further scroll attempts are made and `onSettled` is
  not called.

---

## \[0.2.3\] — 2026-04-05

### Fixed

- **RAF cancellation on unmount.** `performScrollToIndex` now returns
  a `cancel()` function that cancels pending `requestAnimationFrame`
  callbacks. The `Virtualizer` calls `cancel()` in its `cleanup()`
  method on unmount, preventing `scrollToFn` from being called on
  disconnected elements. Previous scroll-to-index reconciliations are
  also cancelled when a new `scrollToIndex` call is made.

---

## \[0.2.2\] — 2026-04-04

### Breaking Changes

- `useFlushSync` option removed from `ReactVirtualizerOptions` — was
  accepted but never implemented.

### Fixed

- `elementsCache` is now cleaned up on `measure()` — prevents stale
  element references after item removal.

### Documentation

- SSR usage with `initialMeasurementsCache` documented in README.
- Next.js App Router example added.

---

## \[0.2.1\] — 2026-04-04

### Fixed

- **`useVirtualizer` snapshot bug.** `useSyncExternalStore` received
  the same `Virtualizer` class-instance reference as snapshot, causing
  `Object.is()` to always return `true` and preventing re-renders in
  Concurrent Mode — fixed by returning a `_stateVersion` counter that
  increments on every `notify()`.
- **Velocity tracking jojo-effect.** Sliding-window velocity calculation
  returned 0 during rapid direction changes (e.g. 200 px down then
  200 px up within 100 ms) — replaced with Exponential Moving Average
  (`alpha = 0.3`) using absolute instant velocity.
- **Smooth scroll interrupt.** User-initiated scroll during a smooth
  `scrollToIndex` animation left deferred measurements waiting for the
  2000 ms timeout — now detects user interrupts via
  `_programmaticScrollPending` flag and flushes deferred measurements
  immediately.

### Changed

- Velocity tracking: replaced sliding-window array with EMA (two
  private fields instead of array allocation, better spike filtering).
- Default `velocityFactor` lowered from 0.05 to 0.01 — produces a
  smoother gradient between min and max overscan instead of binary
  behavior.
- Default scroll-reconciliation timeout lowered from 5000 ms to
  2000 ms — 2 seconds is sufficient for DOM settling, 5 seconds felt
  like frozen UI.
- Velocity unit is now px/s (was px/ms internally).

---

## \[0.2.0\] — 2026-04-04

### Fixed

- Scroll reconciliation: added 5000 ms safety timeout and stability-frame
  check — prevents infinite loops in edge cases (broken layout,
  zero-height containers).
- `indexFromElement`: added `NaN` handling for non-numeric `data-index`
  attributes.

### Added

- `resizeItem(index, size)` method — programmatic resize without DOM
  measurement, with scroll adjustment for items above viewport and
  bounds checking with dev-mode warnings.
- Scroll-frame optimization — skips React re-renders when scroll offset
  changes but visible items remain the same, comparing full virtual
  index arrays.
- Directional overscan — asymmetric overscan based on scroll direction
  (70 % in scroll direction, 30 % opposite), minimum 1 in each
  direction.
- Adaptive overscan — velocity-based dynamic overscan via
  `AdaptiveOverscanOptions` (`{min, max, velocityFactor}`). Fast
  scrolling increases overscan, idle reduces to minimum. Integrates
  with directional overscan (adaptive = how much, directional = where).
- Smooth-scroll measurement throttling — during smooth `scrollToIndex`,
  measurements are limited to items within a buffer around the target
  index. Deferred measurements are flushed when reconciliation settles.
- `initialOffset` option documentation and edge-case handling.
- `indexFromElement()` method documentation.
- `calculateRange()` public-method documentation.

### Changed

- `overscan` option now accepts `number | AdaptiveOverscanOptions`
  (backwards compatible).
- `defaultRangeExtractor` now applies directional overscan based on
  `scrollDirection` property on `Range`.
- Scroll-reconciliation state includes `startedAt` timestamp and
  `stableFrames` counter.
- Virtualizer tracks scroll velocity via EMA (used by adaptive
  overscan).

---

## \[0.1.0\] — Initial release

- Virtualizer core with vertical/horizontal support.
- Multi-lane (grid) layout with `lanes` option.
- Dynamic item measurement via `ResizeObserver`.
- Scroll to index/offset with alignment options.
- `useVirtualizer` and `useWindowVirtualizer` React hooks.
- SSR support with `initialMeasurementsCache`.
