import type { Router } from "@real-router/core";
/**
* Router-coordinated scroll spy (#575).
*
* On `IntersectionObserver` notifications the utility picks the topmost
* visible anchor inside the configured scroll container and emits a forced
* same-route transition with `{ hash, replace: true, force: true, hashChange:
* true }` through `router.navigate(...)`. The URL plugin
* (`@real-router/browser-plugin` or `@real-router/navigation-plugin`) updates
* `state.context.url.hash` so sibling hash-aware `` re-highlights
* via the standard `createActiveRouteSource` pipeline.
*
* **Anti-flicker gates** (RFC §5.2):
* 1. `getTransitionSource(router).getSnapshot().isTransitioning` — skip emits
* while a transition is in-flight (re-entrant lock).
* 2. `coolingDown` — set on a user-driven hash transition (e.g. ``
* click + smooth `scrollIntoView`). Cleared on `scrollend` or after a
* 500ms safety timeout. Spy's own emits are excluded via the synchronous
* `selfEmitting` flag — required so the spy doesn't rate-limit itself.
*
* **Self-healing** (RFC §7.3): if the initial URL contains a hash without a
* matching `id` (e.g. `/page#nonexistent`), the first IO event emitted right
* after observe()-ing picks the topmost real anchor and corrects the URL.
*
* **Hash-only transition pipeline cost** (RFC §5.3): for same-route same-
* params hash-only navigations, `getTransitionPath` returns empty
* `toDeactivate` / `toActivate` arrays, so `runGuards` is a no-op. The only
* work is the URL plugin's `onTransitionSuccess` write and the
* `getTransitionSource` flip — cheap.
*
* **Architecture**: decomposed into 4 private subsystem closure factories
* (`createUrlPluginDetector`, `createCooldown`, `createDebouncer`,
* `createObserverPair`). The main `createScrollSpy` wires them together
* around the shared `silenced` / `destroyed` / `selfEmitting` flags and the
* `flush()` emit logic. Each subsystem owns its state + cleanup; `destroy()`
* delegates to each. See section banners below.
*
* @returns A `ScrollSpy` handle whose `destroy()` is idempotent.
*/
export interface ScrollSpyOptions {
/**
* CSS selector for anchor candidates. Empty string `""` or `undefined`
* disables the spy (returns a NOOP handle). Common values:
* `"[id]"`, `"[id]:is(h1,h2,h3)"`, `"section[id]"`.
*/
selector: string;
/**
* `IntersectionObserver` `rootMargin`. Default
* `"-20% 0px -60% 0px"` — an anchor is considered "active" once it crosses
* into the top 20 % of the viewport (or scroll container).
*/
rootMargin?: string | undefined;
/**
* Lazy getter for the scrollable container. Consulted at creation and
* re-consulted on every reconcile (DOM mutation), so a container that
* MOUNTS or CHANGES after the spy is created is honoured: the
* `IntersectionObserver` root and `MutationObserver` target — both immutable
* once constructed — are rebuilt to match (#780). `null` (or a missing
* getter) falls back to the window viewport (`root: null` on the
* `IntersectionObserver`).
*/
scrollContainer?: (() => HTMLElement | null) | undefined;
}
export interface ScrollSpy {
/** Tear down observer + listeners. Idempotent. */
destroy: () => void;
}
export declare function createScrollSpy(router: Router, options: ScrollSpyOptions): ScrollSpy;