phase 0.0.7 → 0.0.9

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
@@ -35,6 +35,13 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
35
35
  - [createLifecycle](#createlifecycle)
36
36
  - [createScrollProgress](#createscrollprogress)
37
37
  - [createScroll](#createscroll)
38
+ - [createThrottle](#createthrottle)
39
+ - [createDebounce](#createdebounce)
40
+ - [createRenderState](#createrenderstate)
41
+ - [createDevicePixelRatio](#createdevicepixelratio)
42
+ - [createMutation](#createmutation)
43
+ - [createPointer](#createpointer)
44
+ - [whenIdle](#whenidle)
38
45
  - [prefersReducedMotion](#prefersreducedmotion)
39
46
  - [Easing and math](#easing-and-math)
40
47
  - [Choosing a primitive](#choosing-a-primitive)
@@ -46,7 +53,11 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
46
53
  - [usePresence](#usepresence)
47
54
  - [useScrollProgress](#usescrollprogress)
48
55
  - [useScroll](#usescroll)
49
- - [Utility hooks](#utility-hooks)
56
+ - [useThrottledCallback](#usethrottledcallback)
57
+ - [useDebouncedCallback](#usedebouncedcallback)
58
+ - [useMutation](#usemutation)
59
+ - [usePointer](#usepointer)
60
+ - [Observation and utility hooks](#observation-and-utility-hooks)
50
61
  - [React components](#react-components)
51
62
  - [How animations work](#how-animations-work)
52
63
  - [Presence](#presence)
@@ -54,7 +65,8 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
54
65
  - [Swap](#swap)
55
66
  - [Rendering](#rendering)
56
67
  - [Defer](#defer)
57
- - [WhenIdle](#whenidle)
68
+ - [WhenIdle](#whenidle-1)
69
+ - [useIdle](#useidle)
58
70
  - [useWhenIdle](#usewhenidle)
59
71
  - [useRenderState](#userenderstate)
60
72
  - [Guarantees](#guarantees)
@@ -146,11 +158,11 @@ If a gap fails any criterion, phase closes it in the [skill](#agent-skill) (audi
146
158
 
147
159
  ## Entry points
148
160
 
149
- | Import | Contents |
150
- | ------------- | ----------------------------------------------------------------------------------------------------- |
151
- | `phase` | Core primitives: createLoop, createTicker, createSight, createLifecycle, createScrollProgress, errors |
152
- | `phase/ease` | Easing functions and math utilities only |
153
- | `phase/react` | React hooks and components |
161
+ | Import | Contents |
162
+ | ------------- | ------------------------------------------------------------------------------- |
163
+ | `phase` | Framework-agnostic timing, observation, lifecycle, scheduling, math, and errors |
164
+ | `phase/ease` | Easing functions and math utilities only |
165
+ | `phase/react` | React hooks and components |
154
166
 
155
167
  Each entry point is independently tree-shakeable. Importing `phase/ease` in a server component pulls zero browser APIs.
156
168
 
@@ -398,6 +410,157 @@ scroll.stop();
398
410
 
399
411
  The options type is `CreateScrollOptions` (`ScrollOptions` is a `lib.dom` global and must not be shadowed).
400
412
 
413
+ ### createThrottle
414
+
415
+ 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.
416
+
417
+ > **Event-driven, not a loop.** This fires on the trigger and idles otherwise. To cap a continuous render loop, use `fps` on [`createLoop`](#createloop). To think in rates, `interval: 1000 / 20` reads as "at most 20 per second".
418
+
419
+ ```ts
420
+ import { createThrottle } from 'phase';
421
+
422
+ const throttle = createThrottle({
423
+ callback: (state) => socket.emit('cursor', state.x, state.y),
424
+ interval: 50,
425
+ });
426
+
427
+ const pointer = createPointer({ element, onPointer: throttle.call });
428
+
429
+ // throttle.flush() fires a pending trailing call now
430
+ // throttle.cancel() discards it and resets the window
431
+ // cleanup:
432
+ throttle.stop();
433
+ ```
434
+
435
+ When the document hides, a pending call is flushed with the latest value (default) or dropped per `hidden`. Calls made while hidden are recorded but fire nothing until the document is visible again.
436
+
437
+ #### Throttle options
438
+
439
+ | Option | Type | Default | Description |
440
+ | ---------- | ----------------------------------- | --------- | --------------------------------------------- |
441
+ | `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
442
+ | `interval` | `number` | required | Minimum ms between invocations |
443
+ | `edge` | `'leading' \| 'trailing' \| 'both'` | `'both'` | Which edges fire |
444
+ | `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
445
+ | `signal` | `AbortSignal` | — | Stops the throttle when aborted |
446
+
447
+ ### createDebounce
448
+
449
+ Visibility-aware trailing debounce: fires the callback with the latest value once `wait` ms pass without a new call. No timer runs while the document is hidden; the quiet period restarts on return. Use it for work that should wait out a burst, like reallocating canvas buffers after a resize stream settles.
450
+
451
+ ```ts
452
+ import { createDebounce } from 'phase';
453
+
454
+ const debounce = createDebounce({
455
+ callback: (size) => reallocateBuffers(size),
456
+ wait: 250,
457
+ });
458
+
459
+ debounce.call({ width, height });
460
+
461
+ // cleanup:
462
+ debounce.stop();
463
+ ```
464
+
465
+ Same surface as `createThrottle`: `flush()`, `cancel()`, a synchronous `pending` read, and terminal `stop()`.
466
+
467
+ #### Debounce options
468
+
469
+ | Option | Type | Default | Description |
470
+ | ---------- | -------------------- | --------- | --------------------------------------------- |
471
+ | `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
472
+ | `wait` | `number` | required | Quiet period in ms; each call restarts it |
473
+ | `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
474
+ | `signal` | `AbortSignal` | — | Stops the debounce when aborted |
475
+
476
+ ### createRenderState
477
+
478
+ Reports whether the browser is rendering an element or skipping it under `content-visibility: auto`. Use it to pause raw work inside deferred content; `phase` loops already pause themselves.
479
+
480
+ ```ts
481
+ import { createRenderState } from 'phase';
482
+
483
+ const renderState = createRenderState({
484
+ element: el,
485
+ onPhaseChange: (phase) => {
486
+ if (phase === 'skipped') clock.pause();
487
+ else clock.resume();
488
+ },
489
+ });
490
+
491
+ renderState.stop();
492
+ ```
493
+
494
+ It listens to `contentvisibilityautostatechange`, the browser's actual paint decision, without changing layout.
495
+
496
+ ### createDevicePixelRatio
497
+
498
+ Tracks `devicePixelRatio` changes through a shared media-query subscription. Use it for framework-free canvas, WebGL, or worker renderers that own their buffer sizing.
499
+
500
+ ```ts
501
+ import { createDevicePixelRatio } from 'phase';
502
+
503
+ const dpr = createDevicePixelRatio({
504
+ onChange: (value) => renderer.setPixelRatio(Math.min(value, 2)),
505
+ });
506
+
507
+ // dpr.dpr is always current
508
+ dpr.stop();
509
+ ```
510
+
511
+ `useCanvas` handles DPR automatically; use this primitive only when you own the renderer.
512
+
513
+ ### createMutation
514
+
515
+ A lifecycle-aware `MutationObserver`: records are coalesced into one callback per animation frame, observation pauses off-screen by default, and teardown is explicit.
516
+
517
+ ```ts
518
+ import { createMutation } from 'phase';
519
+
520
+ const mutation = createMutation({
521
+ element: list,
522
+ mutation: { childList: true },
523
+ onMutations: (records) => syncItems(records),
524
+ });
525
+
526
+ mutation.stop();
527
+ ```
528
+
529
+ Reserve it for structural or narrow attribute changes. For dimensions, use ResizeObserver-backed `useSize`; reading layout inside `onMutations` still forces a reflow.
530
+
531
+ ### createPointer
532
+
533
+ Tracks pointer position relative to an element, batching high-frequency events into one callback and one bounds read per animation frame. It pauses when the element is off-screen.
534
+
535
+ ```ts
536
+ import { createPointer } from 'phase';
537
+
538
+ const pointer = createPointer({
539
+ element: surface,
540
+ onPointer: (state) => {
541
+ cursor.style.transform = `translate(${state.x}px, ${state.y}px)`;
542
+ },
543
+ });
544
+
545
+ // pointer.state is always current
546
+ pointer.stop();
547
+ ```
548
+
549
+ Use CSS `:hover` for hover state and a gesture library for drag physics. This primitive is for continuous element-relative coordinates.
550
+
551
+ ### whenIdle
552
+
553
+ Runs one callback when the browser is idle, with a timeout fallback for browsers without `requestIdleCallback`. The returned function cancels pending work.
554
+
555
+ ```ts
556
+ import { whenIdle } from 'phase';
557
+
558
+ const cancel = whenIdle(() => warmCache(), { timeout: 2000 });
559
+ cancel();
560
+ ```
561
+
562
+ In React, use `useWhenIdle` for effects, `useIdle` for a boolean, or `WhenIdle` to mount a subtree.
563
+
401
564
  ### prefersReducedMotion
402
565
 
403
566
  Returns `true` when reduced motion is enabled at the OS level. Use it to gate expensive setup or dynamic imports.
@@ -455,19 +618,25 @@ Easing, interpolation, and your value range are three separate concerns. `phase`
455
618
 
456
619
  ## Choosing a primitive
457
620
 
458
- | Need | Use |
459
- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
460
- | Check on-screen visibility | `useSight` (visibility only) |
461
- | Run a frame loop via `phase` | `useLoop` (DOM) / `useCanvas` (canvas) |
462
- | Pause/resume your own loop (WebGL, three.js, Web Worker) | `useLifecycle` (active/paused signal) |
463
- | Animate a single value in render output | `useTween` |
464
- | Animate mount/unmount transitions | `Presence` / `Swap` / `WhenVisible` |
465
- | Skip painting off-screen content (keep in DOM) | `Defer` |
466
- | Defer non-critical UI until the browser is idle | `WhenIdle` / `useIdle` |
467
- | Run a side effect or prefetch when idle | `useWhenIdle` |
468
- | Pause non-`phase` work inside a `Defer` subtree | `useRenderState` |
469
- | Subscribe to scroll, size, or media values reactively | `useScrollProgress` / `useSize` / `useContainerQuery` / `useMediaQuery` |
470
- | Scroll/size/visibility without re-renders? | Same hooks with a callback (`onProgress` / `onResize` / `onVisibilityChange`), read via ref |
621
+ | Need | Use |
622
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
623
+ | Check on-screen visibility | `useSight` (visibility only) |
624
+ | Run a frame loop via `phase` | `useLoop` (DOM) / `useCanvas` (canvas) |
625
+ | Pause/resume your own loop (WebGL, three.js, Web Worker) | `useLifecycle` (active/paused signal) |
626
+ | Animate a single value in render output | `useTween` |
627
+ | Animate mount/unmount transitions | `Presence` / `Swap` / `WhenVisible` |
628
+ | Skip painting off-screen content (keep in DOM) | `Defer` |
629
+ | Defer non-critical UI until the browser is idle | `WhenIdle` / `useIdle` |
630
+ | Run a side effect or prefetch when idle | `useWhenIdle` |
631
+ | Pause non-`phase` work inside a `Defer` subtree | `useRenderState` |
632
+ | React to DOM mutations without synchronous callback storms | `useMutation` |
633
+ | Track element-relative pointer position without per-event layout reads | `usePointer` |
634
+ | Track DPR for a renderer you own | `useDevicePixelRatio` |
635
+ | Check reduced motion for non-`phase` work | `usePrefersReducedMotion` |
636
+ | Subscribe to scroll, size, or media values reactively | `useScrollProgress` / `useSize` / `useContainerQuery` / `useMediaQuery` |
637
+ | Scroll/size/visibility without re-renders? | Same hooks with a callback (`onProgress` / `onResize` / `onVisibilityChange`), read via ref |
638
+ | Rate-limit event-driven work (sockets, workers) | `useThrottledCallback` |
639
+ | Run once after a burst settles (resize, typing) | `useDebouncedCallback` |
471
640
 
472
641
  **`useSight` vs `useLifecycle`:** `useSight` reports pure visibility (for lazy-mounting, analytics, `WhenVisible`). `useLifecycle` folds in reduced motion and a manual pause, so you can't accidentally animate for users who asked not to. If you're gating an animation, use `useLifecycle`. If you're gating content, use `useSight`.
473
642
 
@@ -668,17 +837,94 @@ function Carousel({ children }) {
668
837
 
669
838
  Scrolling writes to the DOM directly with zero re-renders. Read the latest position on demand from `stateRef.current` (e.g. inside a `useLoop` tick), and call `measure()` after changing scrollable content.
670
839
 
671
- ### Utility hooks
840
+ ### useThrottledCallback
841
+
842
+ Wraps `createThrottle` with React lifecycle management. Returns a stable-identity throttled function (with `flush()` and `cancel()` attached) that drops directly into any callback slot and always invokes the latest `callback`.
843
+
844
+ ```tsx
845
+ import { usePointer, useThrottledCallback } from 'phase/react';
846
+
847
+ function LiveCursor() {
848
+ const emit = useThrottledCallback(
849
+ (s: PointerState) => socket.emit('cursor', { x: s.x, y: s.y }),
850
+ { interval: 50 },
851
+ );
852
+ const { ref } = usePointer({ onPointer: emit });
853
+ return <div ref={ref} />;
854
+ }
855
+ ```
672
856
 
673
- | Hook | Purpose |
674
- | ------------------- | ------------------------------------------------------------------------------------- |
675
- | `useSight` | Element visibility as a phase. Pass `onVisibilityChange` for zero-re-render mode |
676
- | `useSize` | Element dimensions via shared ResizeObserver. Pass `onResize` for zero-re-render mode |
677
- | `useContainerQuery` | Breakpoint matching against element width |
678
- | `useScrollProgress` | Element visibility ratio (0–1). Pass `onProgress` for zero-re-render mode |
679
- | `useMediaQuery` | CSS media query subscription (shared MQL pool) |
680
- | `useSyncedRef` | Ref always in sync with latest value |
681
- | `useStableCallback` | Stable-identity function that calls latest closure |
857
+ Unmount and option changes discard a pending trailing call. When the final value must land, flush in your own cleanup: `useEffect(() => () => emit.flush(), [emit])`.
858
+
859
+ ### useDebouncedCallback
860
+
861
+ Wraps `createDebounce` with React lifecycle management. Same shape as `useThrottledCallback`, but fires once `wait` ms pass without a new call.
862
+
863
+ ```tsx
864
+ import { useSize, useDebouncedCallback } from 'phase/react';
865
+
866
+ function SimulationCanvas() {
867
+ const realloc = useDebouncedCallback(
868
+ (size: Size) => reallocateBuffers(size),
869
+ { wait: 250 },
870
+ );
871
+ const { ref } = useSize({ onResize: realloc });
872
+ return <canvas ref={ref} />;
873
+ }
874
+ ```
875
+
876
+ ### useMutation
877
+
878
+ Wraps `createMutation` with ref management and automatic teardown. Mutation records stay imperative—delivered once per animation frame—while only infrequent `observing` / `paused` phase changes re-render.
879
+
880
+ ```tsx
881
+ import { useMutation } from 'phase/react';
882
+
883
+ const { ref, phase } = useMutation({
884
+ mutation: { childList: true },
885
+ onMutations: (records) => syncItems(records),
886
+ });
887
+
888
+ return <ul ref={ref} data-observer-phase={phase} />;
889
+ ```
890
+
891
+ Observation pauses off-screen by default. Set `visibility: 'ignore'` only for document-level coordination that must continue in the background.
892
+
893
+ ### usePointer
894
+
895
+ Element-relative pointer tracking without per-event layout reads or per-frame React state. Position is delivered through `onPointer` and mirrored in `stateRef`; only enter/leave phase changes re-render.
896
+
897
+ ```tsx
898
+ import { usePointer } from 'phase/react';
899
+
900
+ const { ref } = usePointer({
901
+ onPointer: ({ x, y, active }) => {
902
+ cursorRef.current?.style.setProperty(
903
+ 'transform',
904
+ `translate(${x}px, ${y}px)`,
905
+ );
906
+ cursorRef.current?.toggleAttribute('data-active', active);
907
+ },
908
+ });
909
+
910
+ return <div ref={ref}>{children}</div>;
911
+ ```
912
+
913
+ Use it for custom cursors, canvas interaction, and tooltips—not simple hover or drag gestures.
914
+
915
+ ### Observation and utility hooks
916
+
917
+ | Hook | Purpose |
918
+ | ------------------------- | ------------------------------------------------------------------------------------- |
919
+ | `useSight` | Element visibility as a phase. Pass `onVisibilityChange` for zero-re-render mode |
920
+ | `useSize` | Element dimensions via shared ResizeObserver. Pass `onResize` for zero-re-render mode |
921
+ | `useContainerQuery` | Breakpoint matching against element width |
922
+ | `useScrollProgress` | Element visibility ratio (0–1). Pass `onProgress` for zero-re-render mode |
923
+ | `useMediaQuery` | CSS media query subscription (shared MQL pool) |
924
+ | `usePrefersReducedMotion` | Reactive reduced-motion preference for non-`phase` animation |
925
+ | `useDevicePixelRatio` | Reactive DPR for renderers outside `useCanvas` |
926
+ | `useSyncedRef` | Ref always in sync with latest value |
927
+ | `useStableCallback` | Stable-identity function that calls latest closure |
682
928
 
683
929
  `useSight`, `useSize`, and `useScrollProgress` each support a transient mode: pass a callback (`onVisibilityChange`, `onResize`, `onProgress`) and the hook delivers updates via callback with zero re-renders. The reactive state field is omitted from the return type so accessing it is a compile-time error. An always-current ref (`phaseRef`, `sizeRef`, `progressRef`) is available in both modes.
684
930
 
@@ -860,6 +1106,19 @@ import { WhenIdle } from 'phase/react';
860
1106
 
861
1107
  Idle never fires during SSR, so `WhenIdle` children are absent from server HTML. Reserve it for non-critical content. For content that must be crawlable, use `Defer`. Reduced motion is automatic: `data-enter="animate"` is not stamped when reduced motion is preferred.
862
1108
 
1109
+ ### useIdle
1110
+
1111
+ Returns `false`, then flips to `true` once the browser is idle. Use it when the idle signal belongs in render; use `WhenIdle` for a wrapper or `useWhenIdle` for an effect.
1112
+
1113
+ ```tsx
1114
+ import { useIdle } from 'phase/react';
1115
+
1116
+ const idle = useIdle({ timeout: 2000 });
1117
+ return idle ? <SecondaryPanel /> : <Skeleton />;
1118
+ ```
1119
+
1120
+ Like `WhenIdle`, idle-gated content is absent from server HTML and should be non-critical.
1121
+
863
1122
  ### useWhenIdle
864
1123
 
865
1124
  Runs a callback once when the browser is idle after mount (the effect-shaped counterpart to `useIdle`). Use it for side effects (prefetching a chunk, warming a cache) rather than rendering. Cancels on unmount and always calls the latest callback.
@@ -974,45 +1233,49 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
974
1233
  | ------------------------- | ----------------: |
975
1234
  | **Core** | |
976
1235
  | `createTicker` | 834 B |
977
- | `createSight` | 963 B |
978
- | `createLifecycle` | 1.47 kB |
1236
+ | `createSight` | 967 B |
1237
+ | `createLifecycle` | 1.48 kB |
979
1238
  | `createLoop` | 2.59 kB |
980
- | `createScrollProgress` | 878 B |
1239
+ | `createScrollProgress` | 866 B |
981
1240
  | `createRenderState` | 495 B |
982
1241
  | `createDevicePixelRatio` | 544 B |
983
- | `createMutation` | 1.17 kB |
984
- | `createPointer` | 1.26 kB |
1242
+ | `createMutation` | 1.18 kB |
1243
+ | `createPointer` | 1.27 kB |
985
1244
  | `createScroll` | 1.45 kB |
1245
+ | `createThrottle` | 657 B |
1246
+ | `createDebounce` | 559 B |
986
1247
  | `whenIdle` | 409 B |
987
1248
  | `prefersReducedMotion` | 101 B |
988
1249
  | **Ease** | |
989
1250
  | `ease (all)` | 210 B |
990
1251
  | **React** | |
991
- | `useLoop` | 2.81 kB |
992
- | `useLifecycle` | 1.69 kB |
1252
+ | `useLoop` | 2.82 kB |
1253
+ | `useLifecycle` | 1.68 kB |
993
1254
  | `useSight` | 1.18 kB |
994
1255
  | `useCanvas` | 3.44 kB |
995
1256
  | `useMutation` | 1.36 kB |
996
- | `usePointer` | 1.48 kB |
997
- | `useScroll` | 1.71 kB |
998
- | `useTween` | 617 B |
999
- | `usePresence` | 593 B |
1000
- | `useScrollProgress` | 993 B |
1001
- | `useSize` | 384 B |
1002
- | `useContainerQuery` | 357 B |
1003
- | `useMediaQuery` | 245 B |
1257
+ | `usePointer` | 1.47 kB |
1258
+ | `useScroll` | 1.72 kB |
1259
+ | `useThrottledCallback` | 797 B |
1260
+ | `useDebouncedCallback` | 688 B |
1261
+ | `useTween` | 619 B |
1262
+ | `usePresence` | 591 B |
1263
+ | `useScrollProgress` | 997 B |
1264
+ | `useSize` | 378 B |
1265
+ | `useContainerQuery` | 384 B |
1266
+ | `useMediaQuery` | 246 B |
1004
1267
  | `usePrefersReducedMotion` | 272 B |
1005
1268
  | `useDevicePixelRatio` | 231 B |
1006
1269
  | `useSyncedRef` | 22 B |
1007
1270
  | `useStableCallback` | 39 B |
1008
- | `Presence` | 739 B |
1271
+ | `Presence` | 741 B |
1009
1272
  | `WhenVisible` | 1.44 kB |
1010
- | `WhenIdle` | 592 B |
1273
+ | `WhenIdle` | 593 B |
1011
1274
  | `Defer` | 86 B |
1012
1275
  | `useIdle` | 435 B |
1013
- | `useWhenIdle` | 449 B |
1276
+ | `useWhenIdle` | 445 B |
1014
1277
  | `useRenderState` | 527 B |
1015
- | `Swap` | 1.12 kB |
1278
+ | `Swap` | 1.13 kB |
1016
1279
 
1017
1280
  <!-- SIZE-TABLE:END -->
1018
1281
 
@@ -243,10 +243,7 @@ function observeIntersection(options) {
243
243
  * IO options are immutable after construction, so identical options can share.
244
244
  */
245
245
  function getPoolKey(opts) {
246
- const root = opts.root ? "custom" : "null";
247
- const margin = opts.rootMargin ?? "0px";
248
- const threshold = Array.isArray(opts.threshold) ? opts.threshold.join(",") : String(opts.threshold ?? 0);
249
- return root + "|" + margin + "|" + threshold;
246
+ return `${opts.root ? "custom" : "null"}|${opts.rootMargin ?? "0px"}|${Array.isArray(opts.threshold) ? opts.threshold.join(",") : String(opts.threshold ?? 0)}`;
250
247
  }
251
248
  /** Return an existing pool entry for this key, or create and register a new one. */
252
249
  function getOrCreatePoolEntry(key, options) {
@@ -1414,6 +1411,208 @@ function createScroll(options) {
1414
1411
  };
1415
1412
  }
1416
1413
  //#endregion
1417
- export { linkAbortSignal as C, serverContextError as S, createTicker as _, REDUCED_MOTION_QUERY as a, isPhaseError as b, readDpr as c, createScrollProgress as d, createLoop as f, createSight as g, subscribeMediaQuery as h, createMutation as i, subscribeDpr as l, readMediaQuery as m, observeResize as n, prefersReducedMotion as o, createLifecycle as p, createPointer as r, whenIdle as s, createScroll as t, createRenderState as u, PhaseError as v, missingContextError as x, invalidDurationError as y };
1414
+ //#region src/core/throttle/index.ts
1415
+ /**
1416
+ * Frame-aligned, visibility-aware throttle. Leading calls fire synchronously;
1417
+ * a pending trailing call rides a one-shot rAF chain and fires with the latest
1418
+ * value on the first frame at or past `interval`. While the document is hidden
1419
+ * nothing is scheduled: a pending call is flushed or dropped per `hidden`, and
1420
+ * new calls are recorded but deferred until the document is visible again.
1421
+ *
1422
+ * @remarks
1423
+ * `call` takes exactly one value and stores it by reference, so the hot path
1424
+ * never allocates. Trailing calls read the value at fire time.
1425
+ */
1426
+ function createThrottle(options) {
1427
+ if (typeof document === "undefined") serverContextError("createThrottle");
1428
+ const { callback, interval, edge = "both", hidden = "flush", signal } = options;
1429
+ const leading = edge !== "trailing";
1430
+ const trailing = edge !== "leading";
1431
+ let stopped = false;
1432
+ let pending = false;
1433
+ let lastFire = 0;
1434
+ let rafId = 0;
1435
+ let documentVisible = !document.hidden;
1436
+ let latest = void 0;
1437
+ function fire(now) {
1438
+ lastFire = now;
1439
+ pending = false;
1440
+ callback(latest);
1441
+ }
1442
+ function cancelRaf() {
1443
+ if (rafId !== 0) {
1444
+ cancelAnimationFrame(rafId);
1445
+ rafId = 0;
1446
+ }
1447
+ }
1448
+ function tick() {
1449
+ rafId = 0;
1450
+ if (stopped || !pending) return;
1451
+ const now = performance.now();
1452
+ if (now - lastFire >= interval) fire(now);
1453
+ else rafId = requestAnimationFrame(tick);
1454
+ }
1455
+ function scheduleRaf() {
1456
+ if (rafId === 0) rafId = requestAnimationFrame(tick);
1457
+ }
1458
+ function call(value) {
1459
+ if (stopped) return;
1460
+ latest = value;
1461
+ if (!documentVisible) {
1462
+ pending = true;
1463
+ return;
1464
+ }
1465
+ const now = performance.now();
1466
+ if (now - lastFire >= interval) {
1467
+ if (leading) {
1468
+ fire(now);
1469
+ return;
1470
+ }
1471
+ lastFire = now;
1472
+ }
1473
+ if (trailing) {
1474
+ pending = true;
1475
+ scheduleRaf();
1476
+ }
1477
+ }
1478
+ function flush() {
1479
+ if (stopped || !pending) return;
1480
+ cancelRaf();
1481
+ fire(performance.now());
1482
+ }
1483
+ function cancel() {
1484
+ if (stopped) return;
1485
+ cancelRaf();
1486
+ pending = false;
1487
+ lastFire = 0;
1488
+ }
1489
+ function onVisibilityChange() {
1490
+ documentVisible = !document.hidden;
1491
+ if (stopped) return;
1492
+ if (!documentVisible) {
1493
+ cancelRaf();
1494
+ if (pending) if (hidden === "flush") fire(performance.now());
1495
+ else pending = false;
1496
+ } else if (pending) scheduleRaf();
1497
+ }
1498
+ function onPageShow(event) {
1499
+ if (!event.persisted) return;
1500
+ documentVisible = true;
1501
+ if (!stopped && pending) scheduleRaf();
1502
+ }
1503
+ document.addEventListener("visibilitychange", onVisibilityChange);
1504
+ window.addEventListener("pageshow", onPageShow);
1505
+ let unlinkAbort;
1506
+ function stop() {
1507
+ if (stopped) return;
1508
+ stopped = true;
1509
+ unlinkAbort?.();
1510
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1511
+ window.removeEventListener("pageshow", onPageShow);
1512
+ cancelRaf();
1513
+ pending = false;
1514
+ }
1515
+ unlinkAbort = linkAbortSignal(signal, stop);
1516
+ return {
1517
+ call,
1518
+ flush,
1519
+ cancel,
1520
+ get pending() {
1521
+ return pending;
1522
+ },
1523
+ stop
1524
+ };
1525
+ }
1526
+ //#endregion
1527
+ //#region src/core/debounce/index.ts
1528
+ /**
1529
+ * Visibility-aware trailing debounce. Fires the callback with the latest
1530
+ * value once `wait` milliseconds pass without a new call. While the document
1531
+ * is hidden no timer runs: a pending call is flushed or dropped per `hidden`,
1532
+ * and new calls are recorded but wait until the document is visible again,
1533
+ * when the quiet timer restarts.
1534
+ */
1535
+ function createDebounce(options) {
1536
+ if (typeof document === "undefined") serverContextError("createDebounce");
1537
+ const { callback, wait, hidden = "flush", signal } = options;
1538
+ let stopped = false;
1539
+ let pending = false;
1540
+ let timer;
1541
+ let documentVisible = !document.hidden;
1542
+ let latest = void 0;
1543
+ function fire() {
1544
+ pending = false;
1545
+ callback(latest);
1546
+ }
1547
+ function clearTimer() {
1548
+ if (timer !== void 0) {
1549
+ clearTimeout(timer);
1550
+ timer = void 0;
1551
+ }
1552
+ }
1553
+ function onTimer() {
1554
+ timer = void 0;
1555
+ if (stopped || !pending) return;
1556
+ fire();
1557
+ }
1558
+ function startTimer() {
1559
+ clearTimer();
1560
+ timer = setTimeout(onTimer, wait);
1561
+ }
1562
+ function call(value) {
1563
+ if (stopped) return;
1564
+ latest = value;
1565
+ pending = true;
1566
+ if (documentVisible) startTimer();
1567
+ }
1568
+ function flush() {
1569
+ if (stopped || !pending) return;
1570
+ clearTimer();
1571
+ fire();
1572
+ }
1573
+ function cancel() {
1574
+ if (stopped) return;
1575
+ clearTimer();
1576
+ pending = false;
1577
+ }
1578
+ function onVisibilityChange() {
1579
+ documentVisible = !document.hidden;
1580
+ if (stopped) return;
1581
+ if (!documentVisible) {
1582
+ clearTimer();
1583
+ if (pending) if (hidden === "flush") fire();
1584
+ else pending = false;
1585
+ } else if (pending) startTimer();
1586
+ }
1587
+ function onPageShow(event) {
1588
+ if (!event.persisted) return;
1589
+ documentVisible = true;
1590
+ if (!stopped && pending) startTimer();
1591
+ }
1592
+ document.addEventListener("visibilitychange", onVisibilityChange);
1593
+ window.addEventListener("pageshow", onPageShow);
1594
+ let unlinkAbort;
1595
+ function stop() {
1596
+ if (stopped) return;
1597
+ stopped = true;
1598
+ unlinkAbort?.();
1599
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1600
+ window.removeEventListener("pageshow", onPageShow);
1601
+ clearTimer();
1602
+ pending = false;
1603
+ }
1604
+ unlinkAbort = linkAbortSignal(signal, stop);
1605
+ return {
1606
+ call,
1607
+ flush,
1608
+ cancel,
1609
+ get pending() {
1610
+ return pending;
1611
+ },
1612
+ stop
1613
+ };
1614
+ }
1615
+ //#endregion
1616
+ 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 };
1418
1617
 
1419
- //# sourceMappingURL=scroll-DZvWyPQE.js.map
1618
+ //# sourceMappingURL=debounce-BD9K4TW4.js.map