phase 0.0.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -176,7 +176,7 @@ The main primitive. Composes a ticker, visibility observer, and reduced-motion l
176
176
  import { createLoop } from 'phase';
177
177
 
178
178
  const loop = createLoop({
179
- element: el,
179
+ target: el,
180
180
  onTick: (frame) => {
181
181
  // frame.time — browser rAF timestamp
182
182
  // frame.delta — ms since last tick (clamped to 40ms)
@@ -229,7 +229,7 @@ Controls the loop's response when quality degrades. Same three-value pattern as
229
229
 
230
230
  ```ts
231
231
  createLoop({
232
- element: el,
232
+ target: el,
233
233
  onTick: draw,
234
234
  degraded: 'throttle', // default
235
235
  degradedFps: 20, // only accepted when degraded is 'throttle'
@@ -240,7 +240,7 @@ createLoop({
240
240
 
241
241
  | Option | Type | Default | Description |
242
242
  | --------------- | ----------------------------------- | ------------ | ----------------------------------------- |
243
- | `element` | `Element` | required | Element to observe for visibility |
243
+ | `target` | `Element` | required | Element to observe for visibility |
244
244
  | `onTick` | `(frame: FrameState) => void` | required | Called each frame while running |
245
245
  | `fps` | `number` | — | Cap frames per second |
246
246
  | `reducedMotion` | `'pause' \| 'complete' \| 'ignore'` | `'pause'` | Behavior when user prefers reduced motion |
@@ -283,7 +283,7 @@ Answers one question: is this element visible right now? Combines `document.visi
283
283
  import { createSight } from 'phase';
284
284
 
285
285
  const sight = createSight({
286
- element: el,
286
+ target: el,
287
287
  onPhaseChange: (phase, reason) => {
288
288
  // phase: 'visible' | 'hidden' | 'unknown'
289
289
  // reason: 'initial' | 'viewport' | 'document' | 'bfcache' | 'all-hidden'
@@ -303,7 +303,7 @@ Use `createLifecycle` when you own your render loop (a three.js/WebGL renderer,
303
303
  import { createLifecycle } from 'phase';
304
304
 
305
305
  const lifecycle = createLifecycle({
306
- element: canvas,
306
+ target: canvas,
307
307
  onPhaseChange: (phase, reason) => {
308
308
  // phase: 'idle' | 'active' | 'paused' | 'stopped'
309
309
  // reason: 'started' | 'resumed' | 'sight' | 'reduced-motion' | 'manual' | 'disposed' | 'initial'
@@ -337,13 +337,13 @@ Pause priority is `reduced-motion` > `sight` > `manual`.
337
337
 
338
338
  Reports what fraction of an element is currently visible in the viewport (0–1), via the shared IntersectionObserver pool. Zero forced reflows, zero extra observers. Ideal for reveal/opacity effects.
339
339
 
340
- > **Visibility ratio, not scroll offset.** This reports `intersectionRatio` (how much of an element is visible in the viewport), which plateaus for tall elements once they fill it. For a scroll container's _own_ offset (scrollbars, carousels) use [`createScroll`](#createscroll); for CSS-declarative scroll-linked animation use the native `ScrollTimeline` API; for spring/gesture scroll use `motion`.
340
+ > **Visibility ratio, not scroll offset.** This reports `intersectionRatio` (how much of an element is visible in the viewport), which plateaus for tall elements once they fill it. For a scroll container's _own_ offset (scrollbars, carousels, or the page via `target: 'page'` on the hook) use [`createScroll`](#createscroll); for CSS-declarative scroll-linked animation use the native `ScrollTimeline` API; for spring/gesture scroll use `motion`.
341
341
 
342
342
  ```ts
343
343
  import { createScrollProgress } from 'phase';
344
344
 
345
345
  const progress = createScrollProgress({
346
- element: el,
346
+ target: el,
347
347
  onProgress: (ratio) => {
348
348
  el.style.opacity = String(ratio);
349
349
  },
@@ -361,7 +361,7 @@ The `steps` option controls threshold granularity. Default `20` generates 21 eve
361
361
 
362
362
  | Option | Type | Default | Description |
363
363
  | ------------ | ----------------------------- | -------- | ---------------------------------- |
364
- | `element` | `Element` | required | Element to observe |
364
+ | `target` | `Element` | required | Element to observe |
365
365
  | `onProgress` | `(ratio: number) => void` | required | Called at each threshold crossing |
366
366
  | `steps` | `number` | `20` | Number of evenly-spaced thresholds |
367
367
  | `root` | `Element \| Document \| null` | — | IO root element |
@@ -377,7 +377,7 @@ Tracks a scroll container's offset and progress. Reads `scrollLeft`/`scrollTop`
377
377
  import { createScroll } from 'phase';
378
378
 
379
379
  const scroll = createScroll({
380
- element: viewport,
380
+ target: viewport,
381
381
  onScroll: (s) => {
382
382
  // thumb CSS needs `transform-origin: left` so scaleX anchors to the track start
383
383
  thumb.style.transform = `translateX(${s.progressX * (1 - s.visibleX) * 100}%) scaleX(${s.visibleX})`;
@@ -401,7 +401,7 @@ scroll.stop();
401
401
 
402
402
  | Option | Type | Default | Description |
403
403
  | --------------------- | ------------------------------ | --------- | -------------------------------------------------- |
404
- | `element` | `Element` | required | Scroll container to track |
404
+ | `target` | `Element \| Document` | required | Scroll container, or `document` for the page |
405
405
  | `onScroll` | `(state: ScrollState) => void` | required | Called once per rAF frame with position + progress |
406
406
  | `onPhaseChange` | `(phase, reason) => void` | — | Called on phase transitions |
407
407
  | `visibility` | `'pause' \| 'ignore'` | `'pause'` | Pause tracking when off-screen, or ignore |
@@ -410,6 +410,8 @@ scroll.stop();
410
410
 
411
411
  The options type is `CreateScrollOptions` (`ScrollOptions` is a `lib.dom` global and must not be shadowed).
412
412
 
413
+ Pass `document` to track the page scroller. Offsets and geometry then come from `document.scrollingElement`, and since the page is never off-screen, `visibility: 'pause'` reacts to tab visibility alone and creates no `IntersectionObserver`. Use it for scroll progress bars, condensing headers, and scroll-to-top affordances instead of a bare `window` scroll listener.
414
+
413
415
  ### createThrottle
414
416
 
415
417
  Frame-aligned, visibility-aware throttle for event-driven work below frame rate (socket emits, worker messaging, expensive recompute). Leading calls fire synchronously; a pending trailing call fires with the latest value on the first animation frame at or past `interval`. Nothing is scheduled while the trigger is idle or the document is hidden.
@@ -481,7 +483,7 @@ Reports whether the browser is rendering an element or skipping it under `conten
481
483
  import { createRenderState } from 'phase';
482
484
 
483
485
  const renderState = createRenderState({
484
- element: el,
486
+ target: el,
485
487
  onPhaseChange: (phase) => {
486
488
  if (phase === 'skipped') clock.pause();
487
489
  else clock.resume();
@@ -518,7 +520,7 @@ A lifecycle-aware `MutationObserver`: records are coalesced into one callback pe
518
520
  import { createMutation } from 'phase';
519
521
 
520
522
  const mutation = createMutation({
521
- element: list,
523
+ target: list,
522
524
  mutation: { childList: true },
523
525
  onMutations: (records) => syncItems(records),
524
526
  });
@@ -536,7 +538,7 @@ Tracks pointer position relative to an element, batching high-frequency events i
536
538
  import { createPointer } from 'phase';
537
539
 
538
540
  const pointer = createPointer({
539
- element: surface,
541
+ target: surface,
540
542
  onPointer: (state) => {
541
543
  cursor.style.transform = `translate(${state.x}px, ${state.y}px)`;
542
544
  },
@@ -741,12 +743,12 @@ Animates a number from A to B over a duration. Calls `setState` per frame (appro
741
743
  ```tsx
742
744
  import { useTween } from 'phase/react';
743
745
 
744
- const opacity = useTween({ target: isVisible ? 1 : 0, duration: 300 });
746
+ const opacity = useTween({ to: isVisible ? 1 : 0, duration: 300 });
745
747
  ```
746
748
 
747
749
  Use `useTween` for single values where the render is cheap (counters, progress bars, opacity). Use `useLoop` when animating many elements or doing canvas work, since per-frame `setState` doesn't scale.
748
750
 
749
- Reduced motion default: `'complete'` (jumps to target instantly). The value still reaches its destination; it skips the animation.
751
+ Reduced motion default: `'complete'` (jumps to the destination instantly). The value still reaches its destination; it skips the animation.
750
752
 
751
753
  ### usePresence
752
754
 
@@ -1209,13 +1211,14 @@ Every error includes a machine-readable `code` and an actionable message.
1209
1211
  import { PhaseError, isPhaseError } from 'phase';
1210
1212
  ```
1211
1213
 
1212
- | Code | Trigger |
1213
- | ------------------ | ---------------------------------------------------- |
1214
- | `server_context` | Calling a browser-only primitive during SSR |
1215
- | `no_element` | Passing a null or undefined `element` to a primitive |
1216
- | `invalid_duration` | `useTween` duration is zero, negative, or NaN |
1217
- | `ticker_stopped` | Calling `start`/`resume` on a stopped ticker |
1218
- | `missing_context` | `<Swap.State>` used outside `<Swap>` |
1214
+ | Code | Trigger |
1215
+ | -------------------- | --------------------------------------------------- |
1216
+ | `server_context` | Calling a browser-only primitive during SSR |
1217
+ | `no_target` | Passing a null or undefined `target` to a primitive |
1218
+ | `conflicting_target` | Passing both `ref` and `target` to a hook |
1219
+ | `invalid_duration` | `useTween` duration is zero, negative, or NaN |
1220
+ | `ticker_stopped` | Calling `start`/`resume` on a stopped ticker |
1221
+ | `missing_context` | `<Swap.State>` used outside `<Swap>` |
1219
1222
 
1220
1223
  ## Relationship to View Transitions
1221
1224
 
@@ -1232,18 +1235,18 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
1232
1235
  | Export | Size (min+brotli) |
1233
1236
  | ------------------------- | ----------------: |
1234
1237
  | **Core** | |
1235
- | `createTicker` | 846 B |
1236
- | `createSight` | 967 B |
1238
+ | `createTicker` | 847 B |
1239
+ | `createSight` | 970 B |
1237
1240
  | `createLifecycle` | 1.48 kB |
1238
- | `createLoop` | 2.62 kB |
1239
- | `createScrollProgress` | 866 B |
1240
- | `createRenderState` | 495 B |
1241
+ | `createLoop` | 2.6 kB |
1242
+ | `createScrollProgress` | 869 B |
1243
+ | `createRenderState` | 490 B |
1241
1244
  | `createDevicePixelRatio` | 544 B |
1242
- | `createMutation` | 1.18 kB |
1243
- | `createPointer` | 1.27 kB |
1244
- | `createScroll` | 1.45 kB |
1245
- | `createThrottle` | 657 B |
1246
- | `createDebounce` | 559 B |
1245
+ | `createMutation` | 1.17 kB |
1246
+ | `createPointer` | 1.26 kB |
1247
+ | `createScroll` | 1.56 kB |
1248
+ | `createThrottle` | 660 B |
1249
+ | `createDebounce` | 558 B |
1247
1250
  | `whenIdle` | 409 B |
1248
1251
  | `prefersReducedMotion` | 101 B |
1249
1252
  | **Ease** | |
@@ -1255,12 +1258,12 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
1255
1258
  | `useCanvas` | 3.47 kB |
1256
1259
  | `useMutation` | 1.36 kB |
1257
1260
  | `usePointer` | 1.47 kB |
1258
- | `useScroll` | 1.72 kB |
1261
+ | `useScroll` | 1.9 kB |
1259
1262
  | `useThrottledCallback` | 797 B |
1260
- | `useDebouncedCallback` | 688 B |
1261
- | `useTween` | 655 B |
1263
+ | `useDebouncedCallback` | 690 B |
1264
+ | `useTween` | 680 B |
1262
1265
  | `usePresence` | 591 B |
1263
- | `useScrollProgress` | 997 B |
1266
+ | `useScrollProgress` | 1 kB |
1264
1267
  | `useSize` | 378 B |
1265
1268
  | `useContainerQuery` | 384 B |
1266
1269
  | `useMediaQuery` | 246 B |
@@ -1269,13 +1272,13 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
1269
1272
  | `useSyncedRef` | 22 B |
1270
1273
  | `useStableCallback` | 39 B |
1271
1274
  | `Presence` | 741 B |
1272
- | `WhenVisible` | 1.44 kB |
1273
- | `WhenIdle` | 593 B |
1275
+ | `WhenVisible` | 1.43 kB |
1276
+ | `WhenIdle` | 592 B |
1274
1277
  | `Defer` | 86 B |
1275
1278
  | `useIdle` | 435 B |
1276
- | `useWhenIdle` | 445 B |
1277
- | `useRenderState` | 527 B |
1278
- | `Swap` | 1.13 kB |
1279
+ | `useWhenIdle` | 443 B |
1280
+ | `useRenderState` | 521 B |
1281
+ | `Swap` | 1.12 kB |
1279
1282
 
1280
1283
  <!-- SIZE-TABLE:END -->
1281
1284
 
@@ -49,13 +49,20 @@ function serverContextError(fn) {
49
49
  fix: "Move into a useEffect or client-only module."
50
50
  });
51
51
  }
52
- function noElementError(fn) {
53
- throw new PhaseError(`${fn}() requires a DOM element.`, {
54
- code: "no_element",
55
- reason: "The element was null or undefined.",
52
+ function noTargetError(fn) {
53
+ throw new PhaseError(`${fn}() requires a target.`, {
54
+ code: "no_target",
55
+ reason: "The target was null or undefined.",
56
56
  fix: "Pass a mounted Element, or use the React hook which manages the ref."
57
57
  });
58
58
  }
59
+ function conflictingTargetError(fn) {
60
+ throw new PhaseError(`${fn}() received both ref and target.`, {
61
+ code: "conflicting_target",
62
+ reason: "A tracker has one anchor.",
63
+ fix: "Pass ref for an element, or target: 'page' for the page."
64
+ });
65
+ }
59
66
  function invalidDurationError(fn, value) {
60
67
  throw new PhaseError(`${fn}() received an invalid duration: ${value}`, {
61
68
  code: "invalid_duration",
@@ -280,7 +287,7 @@ const createPoolEntry$1 = (options) => {
280
287
  *
281
288
  * @example
282
289
  * const sight = createSight({
283
- * element: el,
290
+ * target: el,
284
291
  * onPhaseChange: (phase) => phase === 'visible' ? loop.start() : loop.pause(),
285
292
  * });
286
293
  * // cleanup:
@@ -291,8 +298,8 @@ const createPoolEntry$1 = (options) => {
291
298
  */
292
299
  function createSight(options) {
293
300
  if (typeof document === "undefined") serverContextError("createSight");
294
- const { element, intersectionOptions, onPhaseChange, signal } = options;
295
- if (!element) noElementError("createSight");
301
+ const { target, intersectionOptions, onPhaseChange, signal } = options;
302
+ if (!target) noTargetError("createSight");
296
303
  let _phase = "unknown";
297
304
  let _reason = "initial";
298
305
  let stopped = false;
@@ -323,7 +330,7 @@ function createSight(options) {
323
330
  document.addEventListener("visibilitychange", onVisibilityChange);
324
331
  window.addEventListener("pageshow", onPageShow);
325
332
  const unobserveIO = observeIntersection({
326
- element,
333
+ element: target,
327
334
  onIntersect: onIntersection,
328
335
  ...intersectionOptions
329
336
  });
@@ -416,7 +423,7 @@ const REDUCED_MOTION_QUERY$1 = "(prefers-reduced-motion: reduce)";
416
423
  *
417
424
  * @example
418
425
  * const lifecycle = createLifecycle({
419
- * element: canvas,
426
+ * target: canvas,
420
427
  * onPhaseChange: (phase) => {
421
428
  * if (phase === 'active') renderer.start();
422
429
  * else renderer.stop();
@@ -427,8 +434,8 @@ const REDUCED_MOTION_QUERY$1 = "(prefers-reduced-motion: reduce)";
427
434
  */
428
435
  function createLifecycle(options) {
429
436
  if (typeof document === "undefined") serverContextError("createLifecycle");
430
- const { element, reducedMotion = "pause", intersectionOptions, start: startMode = "auto", onPhaseChange, signal } = options;
431
- if (!element) noElementError("createLifecycle");
437
+ const { target, reducedMotion = "pause", intersectionOptions, start: startMode = "auto", onPhaseChange, signal } = options;
438
+ if (!target) noTargetError("createLifecycle");
432
439
  let _phase = "idle";
433
440
  let _reason = "initial";
434
441
  let sightVisible = false;
@@ -468,7 +475,7 @@ function createLifecycle(options) {
468
475
  reconcile();
469
476
  }
470
477
  const sight = createSight({
471
- element,
478
+ target,
472
479
  intersectionOptions,
473
480
  onPhaseChange: onSightChange
474
481
  });
@@ -545,7 +552,7 @@ const RECOVERY_RETRY_MS = 2e3;
545
552
  *
546
553
  * @example
547
554
  * const loop = createLoop({
548
- * element: el,
555
+ * target: el,
549
556
  * onTick: (frame) => draw(ctx, frame),
550
557
  * });
551
558
  * // cleanup:
@@ -553,8 +560,8 @@ const RECOVERY_RETRY_MS = 2e3;
553
560
  */
554
561
  function createLoop(options) {
555
562
  if (typeof requestAnimationFrame === "undefined") serverContextError("createLoop");
556
- const { element, onTick, fps: baseFps, reducedMotion = "pause", degraded = "throttle", degradedFps: configuredDegradedFps, intersectionOptions, start: startMode = "auto", onPhaseChange, signal } = options;
557
- if (!element) noElementError("createLoop");
563
+ const { target, onTick, fps: baseFps, reducedMotion = "pause", degraded = "throttle", degradedFps: configuredDegradedFps, intersectionOptions, start: startMode = "auto", onPhaseChange, signal } = options;
564
+ if (!target) noTargetError("createLoop");
558
565
  const degradedFps = degraded === "throttle" ? configuredDegradedFps : void 0;
559
566
  let _phase = "idle";
560
567
  let _reason = "initial";
@@ -675,7 +682,7 @@ function createLoop(options) {
675
682
  reconcileQuality();
676
683
  }
677
684
  const lifecycle = createLifecycle({
678
- element,
685
+ target,
679
686
  reducedMotion: reducedMotion === "pause" ? "pause" : "ignore",
680
687
  intersectionOptions,
681
688
  start: "manual",
@@ -745,7 +752,7 @@ function buildThresholds(steps) {
745
752
  *
746
753
  * @example
747
754
  * const progress = createScrollProgress({
748
- * element: el,
755
+ * target: el,
749
756
  * onProgress: (ratio) => {
750
757
  * el.style.opacity = String(ratio);
751
758
  * },
@@ -755,8 +762,8 @@ function buildThresholds(steps) {
755
762
  */
756
763
  function createScrollProgress(options) {
757
764
  if (typeof IntersectionObserver === "undefined") serverContextError("createScrollProgress");
758
- const { element, onProgress, steps = DEFAULT_STEPS, root, rootMargin, signal } = options;
759
- if (!element) noElementError("createScrollProgress");
765
+ const { target: element, onProgress, steps = DEFAULT_STEPS, root, rootMargin, signal } = options;
766
+ if (!element) noTargetError("createScrollProgress");
760
767
  let _ratio = 0;
761
768
  let stopped = false;
762
769
  const unobserve = observeIntersection({
@@ -802,7 +809,7 @@ function createScrollProgress(options) {
802
809
  *
803
810
  * @example
804
811
  * const render = createRenderState({
805
- * element: el,
812
+ * target: el,
806
813
  * onPhaseChange: (phase) => phase === 'skipped' ? clock.pause() : clock.resume(),
807
814
  * });
808
815
  * // cleanup:
@@ -817,8 +824,8 @@ function createScrollProgress(options) {
817
824
  */
818
825
  function createRenderState(options) {
819
826
  if (typeof document === "undefined") serverContextError("createRenderState");
820
- const { element, onPhaseChange, signal } = options;
821
- if (!element) noElementError("createRenderState");
827
+ const { target: element, onPhaseChange, signal } = options;
828
+ if (!element) noTargetError("createRenderState");
822
829
  let _phase = "rendered";
823
830
  let stopped = false;
824
831
  function onStateChange(event) {
@@ -947,8 +954,8 @@ function prefersReducedMotion() {
947
954
  /** Lifecycle-aware MutationObserver with rAF-coalesced callbacks. */
948
955
  function createMutation(options) {
949
956
  if (typeof document === "undefined") serverContextError("createMutation");
950
- const { element, mutation: mutationInit, onMutations, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
951
- if (!element) noElementError("createMutation");
957
+ const { target: element, mutation: mutationInit, onMutations, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
958
+ if (!element) noTargetError("createMutation");
952
959
  warnReflowStorm(mutationInit);
953
960
  let _phase = "paused";
954
961
  let _reason = "initial";
@@ -1065,8 +1072,8 @@ function warnReflowStorm(opts) {
1065
1072
  /** Lifecycle-aware pointer tracker with rAF-batched `getBoundingClientRect`. */
1066
1073
  function createPointer(options) {
1067
1074
  if (typeof document === "undefined") serverContextError("createPointer");
1068
- const { element, onPointer, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
1069
- if (!element) noElementError("createPointer");
1075
+ const { target: element, onPointer, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
1076
+ if (!element) noTargetError("createPointer");
1070
1077
  let _phase = "idle";
1071
1078
  let _reason = "initial";
1072
1079
  let stopped = false;
@@ -1251,11 +1258,22 @@ function getObserver() {
1251
1258
  * This is to `scroll` + `scrollWidth` what `createPointer` is to `pointermove`
1252
1259
  * + `getBoundingClientRect`: the layout read is batched off the hot path so the
1253
1260
  * per-scroll work is a single cheap position read plus math on cached geometry.
1261
+ *
1262
+ * Pass `document` to track the page. Offsets and geometry then come from
1263
+ * `document.scrollingElement`, and because the page is always in view,
1264
+ * `visibility: 'pause'` reacts to tab visibility alone rather than an
1265
+ * `IntersectionObserver`.
1254
1266
  */
1255
1267
  function createScroll(options) {
1256
1268
  if (typeof document === "undefined") serverContextError("createScroll");
1257
- const { element, onScroll, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
1258
- if (!element) noElementError("createScroll");
1269
+ const { target, onScroll, onPhaseChange, visibility = "pause", intersectionOptions, signal } = options;
1270
+ if (!target) noTargetError("createScroll");
1271
+ let pageDoc;
1272
+ let scroller;
1273
+ if (isDocument(target)) {
1274
+ pageDoc = target;
1275
+ scroller = target.scrollingElement ?? target.documentElement;
1276
+ } else scroller = target;
1259
1277
  let _phase = "paused";
1260
1278
  let _reason = "initial";
1261
1279
  let stopped = false;
@@ -1271,6 +1289,7 @@ function createScroll(options) {
1271
1289
  };
1272
1290
  let rafId = 0;
1273
1291
  let dirty = false;
1292
+ let needsMeasure = false;
1274
1293
  let listenersAttached = false;
1275
1294
  let unobserveRO;
1276
1295
  function setPhase(phase, reason) {
@@ -1281,8 +1300,8 @@ function createScroll(options) {
1281
1300
  }
1282
1301
  function computePosition() {
1283
1302
  const { maxX, maxY } = _state;
1284
- const x = element.scrollLeft;
1285
- const y = element.scrollTop;
1303
+ const x = scroller.scrollLeft;
1304
+ const y = scroller.scrollTop;
1286
1305
  _state.x = x < 0 ? 0 : x > maxX ? maxX : x;
1287
1306
  _state.y = y < 0 ? 0 : y > maxY ? maxY : y;
1288
1307
  _state.progressX = maxX > 0 ? _state.x / maxX : 0;
@@ -1290,10 +1309,10 @@ function createScroll(options) {
1290
1309
  }
1291
1310
  function measure() {
1292
1311
  if (stopped || !listenersAttached) return;
1293
- const scrollWidth = element.scrollWidth;
1294
- const clientWidth = element.clientWidth;
1295
- const scrollHeight = element.scrollHeight;
1296
- const clientHeight = element.clientHeight;
1312
+ const scrollWidth = scroller.scrollWidth;
1313
+ const clientWidth = scroller.clientWidth;
1314
+ const scrollHeight = scroller.scrollHeight;
1315
+ const clientHeight = scroller.clientHeight;
1297
1316
  const maxX = scrollWidth - clientWidth;
1298
1317
  const maxY = scrollHeight - clientHeight;
1299
1318
  _state.maxX = maxX > 0 ? maxX : 0;
@@ -1305,7 +1324,14 @@ function createScroll(options) {
1305
1324
  }
1306
1325
  function flush() {
1307
1326
  rafId = 0;
1308
- if (!dirty || stopped) return;
1327
+ if (stopped) return;
1328
+ if (needsMeasure) {
1329
+ needsMeasure = false;
1330
+ dirty = false;
1331
+ measure();
1332
+ return;
1333
+ }
1334
+ if (!dirty) return;
1309
1335
  dirty = false;
1310
1336
  computePosition();
1311
1337
  onScroll(_state);
@@ -1320,6 +1346,7 @@ function createScroll(options) {
1320
1346
  rafId = 0;
1321
1347
  }
1322
1348
  dirty = false;
1349
+ needsMeasure = false;
1323
1350
  }
1324
1351
  function onScrollEvent() {
1325
1352
  if (stopped) return;
@@ -1328,26 +1355,33 @@ function createScroll(options) {
1328
1355
  }
1329
1356
  function onROResize() {
1330
1357
  if (stopped) return;
1358
+ if (pageDoc) {
1359
+ needsMeasure = true;
1360
+ scheduleFlush();
1361
+ return;
1362
+ }
1331
1363
  measure();
1332
1364
  }
1333
1365
  function attachListeners() {
1334
1366
  if (listenersAttached) return;
1335
1367
  listenersAttached = true;
1336
- element.addEventListener("scroll", onScrollEvent, { passive: true });
1337
- unobserveRO = observeResize(element, onROResize);
1368
+ target.addEventListener("scroll", onScrollEvent, { passive: true });
1369
+ unobserveRO = observeResize(scroller, onROResize);
1370
+ if (pageDoc) window.addEventListener("resize", onROResize);
1338
1371
  measure();
1339
1372
  }
1340
1373
  function detachListeners() {
1341
1374
  if (!listenersAttached) return;
1342
1375
  listenersAttached = false;
1343
- element.removeEventListener("scroll", onScrollEvent);
1376
+ target.removeEventListener("scroll", onScrollEvent);
1344
1377
  unobserveRO?.();
1345
1378
  unobserveRO = void 0;
1379
+ if (pageDoc) window.removeEventListener("resize", onROResize);
1346
1380
  cancelFlush();
1347
1381
  }
1348
1382
  let cleanupVisibility;
1349
1383
  if (visibility === "pause") {
1350
- let elementInView = false;
1384
+ let elementInView = pageDoc !== void 0;
1351
1385
  let documentVisible = !document.hidden;
1352
1386
  function recompute() {
1353
1387
  if (stopped) return;
@@ -1371,8 +1405,8 @@ function createScroll(options) {
1371
1405
  }
1372
1406
  document.addEventListener("visibilitychange", onVisChange);
1373
1407
  window.addEventListener("pageshow", onPageShow);
1374
- const unobserveIO = observeIntersection({
1375
- element,
1408
+ const unobserveIO = pageDoc ? void 0 : observeIntersection({
1409
+ element: scroller,
1376
1410
  onIntersect: (entry) => {
1377
1411
  elementInView = entry.isIntersecting;
1378
1412
  recompute();
@@ -1382,8 +1416,9 @@ function createScroll(options) {
1382
1416
  cleanupVisibility = () => {
1383
1417
  document.removeEventListener("visibilitychange", onVisChange);
1384
1418
  window.removeEventListener("pageshow", onPageShow);
1385
- unobserveIO();
1419
+ unobserveIO?.();
1386
1420
  };
1421
+ recompute();
1387
1422
  } else {
1388
1423
  setPhase("tracking", "started");
1389
1424
  attachListeners();
@@ -1413,6 +1448,10 @@ function createScroll(options) {
1413
1448
  stop
1414
1449
  };
1415
1450
  }
1451
+ const DOCUMENT_NODE = 9;
1452
+ function isDocument(target) {
1453
+ return target.nodeType === DOCUMENT_NODE;
1454
+ }
1416
1455
  //#endregion
1417
1456
  //#region src/core/throttle/index.ts
1418
1457
  /**
@@ -1616,6 +1655,6 @@ function createDebounce(options) {
1616
1655
  };
1617
1656
  }
1618
1657
  //#endregion
1619
- export { missingContextError as C, isPhaseError as S, linkAbortSignal as T, subscribeMediaQuery as _, createPointer as a, PhaseError as b, prefersReducedMotion as c, subscribeDpr as d, createRenderState as f, readMediaQuery as g, createLifecycle as h, observeResize as i, whenIdle as l, createLoop as m, createThrottle as n, createMutation as o, createScrollProgress as p, createScroll as r, REDUCED_MOTION_QUERY as s, createDebounce as t, readDpr as u, createSight as v, serverContextError as w, invalidDurationError as x, createTicker as y };
1658
+ export { isPhaseError as C, linkAbortSignal as E, invalidDurationError as S, serverContextError as T, subscribeMediaQuery as _, createPointer as a, PhaseError as b, prefersReducedMotion as c, subscribeDpr as d, createRenderState as f, readMediaQuery as g, createLifecycle as h, observeResize as i, whenIdle as l, createLoop as m, createThrottle as n, createMutation as o, createScrollProgress as p, createScroll as r, REDUCED_MOTION_QUERY as s, createDebounce as t, readDpr as u, createSight as v, missingContextError as w, conflictingTargetError as x, createTicker as y };
1620
1659
 
1621
- //# sourceMappingURL=debounce-C0SgiTfm.js.map
1660
+ //# sourceMappingURL=debounce-V_5xqZSY.js.map