@n-uf/motion 26.11.4 → 26.11.6

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.
@@ -464,4 +464,248 @@ interface DwellMachineOptions {
464
464
  /** A dwell machine on a clock: the dwell timer is armed for you. */
465
465
  declare function createDwellMachine<K = string, P = unknown>(options: DwellMachineOptions): Machine<DwellState<K, P>, DwellInput<K, P>>;
466
466
 
467
- export { type CancelReason, type CoalescerOptions, DWELL_DEFAULTS, DWELL_IDLE, type Deadline, type DwellConfig, type DwellInput, type DwellMachineOptions, type DwellState, type FrameCoalescer, type InputGesture, type InputGestureType, type Machine, type MachineOptions, type ModifierDrag, type ModifierKey, type Modifiers, NO_MODIFIERS, type Platform, type PointerKind, RECOGNIZER_DEFAULTS, type RawInputEvent, type RawPointerEvent, type RawPointerType, type RawSafariGestureEvent, type RawWheelEvent, type Recognizer, type RecognizerOptions, type RecognizerTimers, type SurfacePointer, WHEEL_DEFAULTS, WHEEL_PAN_DEFAULTS, type WheelIntent, type WheelOptions, type WheelPanOptions, classifyWheel, createDwellMachine, createFrameCoalescer, createMachine, createRecognizer, dampWheelPan, defaultDragModifier, dwellDeadline, dwellReducer, isHeld, modHeld, platformButton, resolveDragModifier, toModifiers, wheelDeltaPx };
467
+ /**
468
+ * A touch press on something draggable that sits in a scrolling page: is it a drag, or a scroll?
469
+ *
470
+ * ```text
471
+ * idle ──press──► pressed ──move: mostly along the scroll direction, ≥ scrollEscape──► scrolled (never lifts)
472
+ * │ ──move: mostly across it, ≥ lift px────────────────────────────► lifted
473
+ * │ ──tick: held still holdMs─────────────────────────────────────► lifted (where it was pressed)
474
+ * └ ──move: along it short of the escape, or across it below lift px: stays pressed
475
+ * any ──release──► idle
476
+ * ```
477
+ *
478
+ * Before the hold has elapsed, a finger that travels mostly along the page's scroll direction is a scroll: the touch
479
+ * goes back to the page and stays a scroll until it is released; short of the escape it waits, never lifting.
480
+ * Travel past the lift distance mostly across the scroll direction is a deliberate drag. Once the hold has elapsed
481
+ * the press lifts where it is. `scrolled` and `lifted`
482
+ * hold until `release`.
483
+ */
484
+ /** Tunables of the touch press. */
485
+ interface TouchPressConfig {
486
+ /** px of travel that lifts a press that is not a scroll. Default 6. */
487
+ readonly lift: number;
488
+ /** px of travel along the scroll direction that makes the press a scroll. Default 10. */
489
+ readonly scrollEscape: number;
490
+ /** The page's scroll direction: `"y"` (a vertical page, the default) or `"x"`. */
491
+ readonly scroll: "x" | "y";
492
+ /** ms a still finger holds before the press lifts where it is. Default 220. */
493
+ readonly holdMs: number;
494
+ }
495
+ /** Defaults of {@link TouchPressConfig}. */
496
+ declare const TOUCH_PRESS_DEFAULTS: TouchPressConfig;
497
+ /** The touch press's state. `from` is where the finger went down, `at` where it lifted. */
498
+ type TouchPressState = {
499
+ readonly phase: "idle";
500
+ } | {
501
+ readonly phase: "pressed";
502
+ readonly from: Point;
503
+ readonly since: number;
504
+ } | {
505
+ readonly phase: "lifted";
506
+ readonly from: Point;
507
+ readonly at: Point;
508
+ } | {
509
+ readonly phase: "scrolled";
510
+ readonly from: Point;
511
+ };
512
+ /** Events of the touch press. `t` is the clock time in ms. */
513
+ type TouchPressInput = {
514
+ readonly type: "press";
515
+ readonly t: number;
516
+ readonly point: Point;
517
+ } | {
518
+ readonly type: "move";
519
+ readonly point: Point;
520
+ } | {
521
+ readonly type: "tick";
522
+ readonly t: number;
523
+ } | {
524
+ readonly type: "release";
525
+ };
526
+ /** The idle state (the same object every time). */
527
+ declare const TOUCH_PRESS_IDLE: TouchPressState;
528
+ /** The touch press's pure transition function. */
529
+ declare function touchPressReducer(state: TouchPressState, event: TouchPressInput, config?: TouchPressConfig): TouchPressState;
530
+ /** When a pressed state lifts by holding, as a timer event; `null` outside `pressed`. */
531
+ declare function touchPressDeadline(state: TouchPressState, config?: TouchPressConfig): Deadline<TouchPressInput> | null;
532
+ /** Options of {@link createTouchPress}. */
533
+ interface TouchPressOptions {
534
+ readonly clock: TimerSource;
535
+ readonly config?: Partial<TouchPressConfig>;
536
+ }
537
+ /** A touch press on a clock: the hold timer is armed for you. */
538
+ declare function createTouchPress(options: TouchPressOptions): Machine<TouchPressState, TouchPressInput>;
539
+
540
+ /**
541
+ * A paged swipe: a trackpad's two-finger scroll or a finger swipes from one page to the previous or the next, the
542
+ * pages following it, and commits or snaps back when it ends.
543
+ *
544
+ * ```text
545
+ * idle ──wheel / touch-move (arming rules pass)──► armed ──travel ≥ thresholdPx──► tracking
546
+ * ▲ │ │
547
+ * │ rule breach / idle / end gesture end
548
+ * │ ▼ ▼
549
+ * └──────── lockoutMs elapsed ◄── lockout ◄── settled (commit) ◄───────────── settling
550
+ * └──────────────────────────────────────── settled (cancel) ◄───────────────────┤
551
+ * └──────────────────────────────────────── cancel (transition stopped) ◄────────┘
552
+ * ```
553
+ *
554
+ * - **Arming:** nothing else owns the pointer (`blocked`), the movement runs mostly along the swipe (more than twice
555
+ * the cross movement), no Ctrl (a pinch), the configured modifier is held, nothing under the pointer can still
556
+ * scroll that way (`canScrollFurther`), and there is a page on that side (or `wrap`). A wheel run that breaks a
557
+ * rule belongs to scrolling until the wheel has been idle `idleMs`.
558
+ * - **Tracking:** `progress` is the travel over the page size, −1 to 1, held at 0 towards a side with no page.
559
+ * - **Ending:** a touch ends when the finger lifts, a wheel run after `idleMs` without samples. It commits when
560
+ * `|progress| ≥ commitFraction`, or the peak speed towards the target reached `commitVelocity` (the peak starts
561
+ * over when the travel crosses zero, so a fast fling out and a pull back does not commit the way back).
562
+ * - **Settling** lasts as long as the host's page transition; the host sends `settled` when it is done (or `cancel`,
563
+ * which goes straight back to idle, when it stops the transition). After a commit, `lockout` swallows the
564
+ * trackpad's momentum for `lockoutMs`, and the run stays claimed after it.
565
+ *
566
+ * Pure and timer-free: {@link pagedSwipeDeadline} says when the next `tick` is due ({@link createPagedSwipe} arms it).
567
+ * Signs: a wheel delta > 0 along the swipe heads to `next`; a finger moving towards smaller x (or y) heads to `next`.
568
+ */
569
+ /** Which side of the current page a swipe heads to. */
570
+ type SwipeSide = "prev" | "next";
571
+ /** Tunables of the paged swipe. */
572
+ interface PagedSwipeConfig {
573
+ /** px of travel before an armed swipe tracks. Default 24. */
574
+ readonly thresholdPx: number;
575
+ /** The share of the page size that commits at the end. Default 0.35. */
576
+ readonly commitFraction: number;
577
+ /** px/ms: a peak speed towards the target that commits at the end. Default 0.6. */
578
+ readonly commitVelocity: number;
579
+ /** ms without a wheel sample that ends a wheel run. Default 120. */
580
+ readonly idleMs: number;
581
+ /** ms after a commit during which wheel samples are swallowed (momentum). Default 350. */
582
+ readonly lockoutMs: number;
583
+ /** Past the last page comes the first, and back. Default false. */
584
+ readonly wrap: boolean;
585
+ /** The page size in px when no sample or context gives one. Default 800. */
586
+ readonly size: number;
587
+ /** A modifier the wheel needs held to swipe (`null`: none). Touch never needs one. Default null. */
588
+ readonly modifier: ModifierKey | null;
589
+ /** The swipe's direction: `"x"` (the default) or `"y"`. */
590
+ readonly along: "x" | "y";
591
+ }
592
+ /** Defaults of {@link PagedSwipeConfig}. */
593
+ declare const PAGED_SWIPE_DEFAULTS: PagedSwipeConfig;
594
+ /** What the host knows about the pages now. */
595
+ interface PagedSwipeContext {
596
+ readonly hasPrev: boolean;
597
+ readonly hasNext: boolean;
598
+ /** The page size in px, or null (the config's). */
599
+ readonly size: number | null;
600
+ /** Something else owns the pointer (a drag): nothing arms, and a swipe in progress gives way. */
601
+ readonly blocked: boolean;
602
+ }
603
+ /** The context before the host has said anything. */
604
+ declare const PAGED_SWIPE_EMPTY_CONTEXT: PagedSwipeContext;
605
+ type Input = "wheel" | "touch";
606
+ interface Base {
607
+ readonly context: PagedSwipeContext;
608
+ /** −1 to 1; negative towards `prev`. */
609
+ readonly progress: number;
610
+ readonly target: SwipeSide | null;
611
+ }
612
+ /** The paged swipe's state. Every phase has a way back to `idle`. */
613
+ type PagedSwipeState = (Base & {
614
+ readonly phase: "idle";
615
+ /** The last wheel sample of the current run (null: none, or the run went idle). */
616
+ readonly lastWheel: number | null;
617
+ /** This wheel run broke the arming rules (or committed): it cannot arm until the wheel is idle `idleMs`. */
618
+ readonly claimed: boolean;
619
+ }) | (Base & {
620
+ readonly phase: "armed";
621
+ readonly input: Input;
622
+ /** Where the finger went down (touch). */
623
+ readonly origin: {
624
+ readonly x: number;
625
+ readonly y: number;
626
+ };
627
+ /** Wheel travel along the swipe and across it so far. */
628
+ readonly travel: number;
629
+ readonly cross: number;
630
+ readonly last: number;
631
+ }) | (Base & {
632
+ readonly phase: "tracking";
633
+ readonly input: Input;
634
+ readonly origin: {
635
+ readonly x: number;
636
+ readonly y: number;
637
+ };
638
+ readonly travel: number;
639
+ readonly last: number;
640
+ /** The highest speed (px/ms) towards the current target. */
641
+ readonly peak: number;
642
+ }) | (Base & {
643
+ readonly phase: "settling";
644
+ readonly input: Input;
645
+ readonly outcome: "commit" | "cancel";
646
+ readonly ended: number;
647
+ readonly lastWheel: number;
648
+ }) | (Base & {
649
+ readonly phase: "lockout";
650
+ readonly until: number;
651
+ readonly lastWheel: number;
652
+ });
653
+ /** Events of the paged swipe. `t` is the clock time in ms. */
654
+ type PagedSwipeInput = {
655
+ readonly type: "wheel";
656
+ readonly dx: number;
657
+ readonly dy: number;
658
+ readonly t: number;
659
+ readonly modifiers: Modifiers;
660
+ /** Something under the pointer can still scroll that way: no arming. */
661
+ readonly canScrollFurther?: boolean;
662
+ readonly size?: number;
663
+ } | {
664
+ readonly type: "touch-start";
665
+ readonly x: number;
666
+ readonly y: number;
667
+ readonly t: number;
668
+ readonly size?: number;
669
+ } | {
670
+ readonly type: "touch-move";
671
+ readonly x: number;
672
+ readonly y: number;
673
+ readonly t: number;
674
+ readonly canScrollFurther?: boolean;
675
+ readonly size?: number;
676
+ } | {
677
+ readonly type: "touch-end";
678
+ readonly t: number;
679
+ } | {
680
+ readonly type: "tick";
681
+ readonly t: number;
682
+ } | {
683
+ readonly type: "settled";
684
+ } | {
685
+ readonly type: "cancel";
686
+ } | {
687
+ readonly type: "block";
688
+ readonly on: boolean;
689
+ } | {
690
+ readonly type: "context";
691
+ readonly hasPrev: boolean;
692
+ readonly hasNext: boolean;
693
+ readonly size?: number;
694
+ };
695
+ /** The idle state with no context. */
696
+ declare const PAGED_SWIPE_IDLE: PagedSwipeState;
697
+ /** Travel to progress: over the page size, −1 to 1, 0 towards a side with no page (unless `wrap`). */
698
+ declare function swipeProgress(travel: number, size: number, c: PagedSwipeContext, wrap: boolean): number;
699
+ /** The paged swipe's pure transition function. Unmatched pairs return the state unchanged. */
700
+ declare function pagedSwipeReducer(state: PagedSwipeState, event: PagedSwipeInput, config?: PagedSwipeConfig): PagedSwipeState;
701
+ /** When the next `tick` is due: a wheel run's idle end, or the end of the lockout; `null` when none is. */
702
+ declare function pagedSwipeDeadline(state: PagedSwipeState, config?: PagedSwipeConfig): Deadline<PagedSwipeInput> | null;
703
+ /** Options of {@link createPagedSwipe}. */
704
+ interface PagedSwipeOptions {
705
+ readonly clock: TimerSource;
706
+ readonly config?: Partial<PagedSwipeConfig>;
707
+ }
708
+ /** A paged swipe on a clock: the idle and lockout timers are armed for you. */
709
+ declare function createPagedSwipe(options: PagedSwipeOptions): Machine<PagedSwipeState, PagedSwipeInput>;
710
+
711
+ export { type CancelReason, type CoalescerOptions, DWELL_DEFAULTS, DWELL_IDLE, type Deadline, type DwellConfig, type DwellInput, type DwellMachineOptions, type DwellState, type FrameCoalescer, type InputGesture, type InputGestureType, type Machine, type MachineOptions, type ModifierDrag, type ModifierKey, type Modifiers, NO_MODIFIERS, PAGED_SWIPE_DEFAULTS, PAGED_SWIPE_EMPTY_CONTEXT, PAGED_SWIPE_IDLE, type PagedSwipeConfig, type PagedSwipeContext, type PagedSwipeInput, type PagedSwipeOptions, type PagedSwipeState, type Platform, type PointerKind, RECOGNIZER_DEFAULTS, type RawInputEvent, type RawPointerEvent, type RawPointerType, type RawSafariGestureEvent, type RawWheelEvent, type Recognizer, type RecognizerOptions, type RecognizerTimers, type SurfacePointer, type SwipeSide, TOUCH_PRESS_DEFAULTS, TOUCH_PRESS_IDLE, type TouchPressConfig, type TouchPressInput, type TouchPressOptions, type TouchPressState, WHEEL_DEFAULTS, WHEEL_PAN_DEFAULTS, type WheelIntent, type WheelOptions, type WheelPanOptions, classifyWheel, createDwellMachine, createFrameCoalescer, createMachine, createPagedSwipe, createRecognizer, createTouchPress, dampWheelPan, defaultDragModifier, dwellDeadline, dwellReducer, isHeld, modHeld, pagedSwipeDeadline, pagedSwipeReducer, platformButton, resolveDragModifier, swipeProgress, toModifiers, touchPressDeadline, touchPressReducer, wheelDeltaPx };
package/dist/gestures.js CHANGED
@@ -2,47 +2,71 @@ import {
2
2
  DWELL_DEFAULTS,
3
3
  DWELL_IDLE,
4
4
  NO_MODIFIERS,
5
+ PAGED_SWIPE_DEFAULTS,
6
+ PAGED_SWIPE_EMPTY_CONTEXT,
7
+ PAGED_SWIPE_IDLE,
5
8
  RECOGNIZER_DEFAULTS,
9
+ TOUCH_PRESS_DEFAULTS,
10
+ TOUCH_PRESS_IDLE,
6
11
  WHEEL_DEFAULTS,
7
12
  WHEEL_PAN_DEFAULTS,
8
13
  classifyWheel,
9
14
  createDwellMachine,
10
15
  createFrameCoalescer,
11
16
  createMachine,
17
+ createPagedSwipe,
12
18
  createRecognizer,
19
+ createTouchPress,
13
20
  dampWheelPan,
14
21
  defaultDragModifier,
15
22
  dwellDeadline,
16
23
  dwellReducer,
17
24
  isHeld,
18
25
  modHeld,
26
+ pagedSwipeDeadline,
27
+ pagedSwipeReducer,
19
28
  platformButton,
20
29
  resolveDragModifier,
30
+ swipeProgress,
21
31
  toModifiers,
32
+ touchPressDeadline,
33
+ touchPressReducer,
22
34
  wheelDeltaPx
23
- } from "./chunk-C4M62FUT.js";
35
+ } from "./chunk-KVFX5V4Y.js";
24
36
  import "./chunk-OOKRBVMB.js";
25
37
  export {
26
38
  DWELL_DEFAULTS,
27
39
  DWELL_IDLE,
28
40
  NO_MODIFIERS,
41
+ PAGED_SWIPE_DEFAULTS,
42
+ PAGED_SWIPE_EMPTY_CONTEXT,
43
+ PAGED_SWIPE_IDLE,
29
44
  RECOGNIZER_DEFAULTS,
45
+ TOUCH_PRESS_DEFAULTS,
46
+ TOUCH_PRESS_IDLE,
30
47
  WHEEL_DEFAULTS,
31
48
  WHEEL_PAN_DEFAULTS,
32
49
  classifyWheel,
33
50
  createDwellMachine,
34
51
  createFrameCoalescer,
35
52
  createMachine,
53
+ createPagedSwipe,
36
54
  createRecognizer,
55
+ createTouchPress,
37
56
  dampWheelPan,
38
57
  defaultDragModifier,
39
58
  dwellDeadline,
40
59
  dwellReducer,
41
60
  isHeld,
42
61
  modHeld,
62
+ pagedSwipeDeadline,
63
+ pagedSwipeReducer,
43
64
  platformButton,
44
65
  resolveDragModifier,
66
+ swipeProgress,
45
67
  toModifiers,
68
+ touchPressDeadline,
69
+ touchPressReducer,
46
70
  wheelDeltaPx
47
71
  };
48
72
  //# sourceMappingURL=gestures.js.map
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export { F as FrameInfo, a as FrameLoop, b as FrameLoopOptions, c as FrameStats,
4
4
  export { EffectCleanup, EffectOptions, Equals, ReadonlySignal, Signal, SignalOptions, batch, computed, effect, isSignal, signal, untracked } from './signals.js';
5
5
  export { Animator, AnimatorValue, Easing, MotionOptions, MotionValue, SETTLE_DEFAULTS, SETTLE_TEMPO, SPRING_DEFAULTS, SetOptions, Settle, SettleConfig, Spring, SpringConfig, SpringParams, SpringState, Tween, TweenConfig, createSettle, createSpring, createTween, cubicBezier, ease, easeIn, easeInOut, easeInOutCubic, easeOut, easeOutCubic, linear, motionValue, resolveSpring, settleValue, springStep, springValue, tweenValue } from './values.js';
6
6
  export { P as PopIn, R as RevealFrame, p as popIn, r as reveal } from './reveal-my7U6UyE.js';
7
- export { CancelReason, CoalescerOptions, DWELL_DEFAULTS, DWELL_IDLE, Deadline, DwellConfig, DwellInput, DwellMachineOptions, DwellState, FrameCoalescer, InputGesture, InputGestureType, Machine, MachineOptions, ModifierDrag, ModifierKey, Modifiers, NO_MODIFIERS, Platform, PointerKind, RECOGNIZER_DEFAULTS, RawInputEvent, RawPointerEvent, RawPointerType, RawSafariGestureEvent, RawWheelEvent, Recognizer, RecognizerOptions, RecognizerTimers, SurfacePointer, WHEEL_DEFAULTS, WHEEL_PAN_DEFAULTS, WheelIntent, WheelOptions, WheelPanOptions, classifyWheel, createDwellMachine, createFrameCoalescer, createMachine, createRecognizer, dampWheelPan, defaultDragModifier, dwellDeadline, dwellReducer, isHeld, modHeld, platformButton, resolveDragModifier, toModifiers, wheelDeltaPx } from './gestures.js';
7
+ export { CancelReason, CoalescerOptions, DWELL_DEFAULTS, DWELL_IDLE, Deadline, DwellConfig, DwellInput, DwellMachineOptions, DwellState, FrameCoalescer, InputGesture, InputGestureType, Machine, MachineOptions, ModifierDrag, ModifierKey, Modifiers, NO_MODIFIERS, PAGED_SWIPE_DEFAULTS, PAGED_SWIPE_EMPTY_CONTEXT, PAGED_SWIPE_IDLE, PagedSwipeConfig, PagedSwipeContext, PagedSwipeInput, PagedSwipeOptions, PagedSwipeState, Platform, PointerKind, RECOGNIZER_DEFAULTS, RawInputEvent, RawPointerEvent, RawPointerType, RawSafariGestureEvent, RawWheelEvent, Recognizer, RecognizerOptions, RecognizerTimers, SurfacePointer, SwipeSide, TOUCH_PRESS_DEFAULTS, TOUCH_PRESS_IDLE, TouchPressConfig, TouchPressInput, TouchPressOptions, TouchPressState, WHEEL_DEFAULTS, WHEEL_PAN_DEFAULTS, WheelIntent, WheelOptions, WheelPanOptions, classifyWheel, createDwellMachine, createFrameCoalescer, createMachine, createPagedSwipe, createRecognizer, createTouchPress, dampWheelPan, defaultDragModifier, dwellDeadline, dwellReducer, isHeld, modHeld, pagedSwipeDeadline, pagedSwipeReducer, platformButton, resolveDragModifier, swipeProgress, toModifiers, touchPressDeadline, touchPressReducer, wheelDeltaPx } from './gestures.js';
8
8
  export { A as Animatable, P as Point, R as Rect, V as Vector, W as Widen, s as shapeEquals } from './shapes-EbQlwbCu.js';
package/dist/index.js CHANGED
@@ -27,25 +27,37 @@ import {
27
27
  DWELL_DEFAULTS,
28
28
  DWELL_IDLE,
29
29
  NO_MODIFIERS,
30
+ PAGED_SWIPE_DEFAULTS,
31
+ PAGED_SWIPE_EMPTY_CONTEXT,
32
+ PAGED_SWIPE_IDLE,
30
33
  RECOGNIZER_DEFAULTS,
34
+ TOUCH_PRESS_DEFAULTS,
35
+ TOUCH_PRESS_IDLE,
31
36
  WHEEL_DEFAULTS,
32
37
  WHEEL_PAN_DEFAULTS,
33
38
  classifyWheel,
34
39
  createDwellMachine,
35
40
  createFrameCoalescer,
36
41
  createMachine,
42
+ createPagedSwipe,
37
43
  createRecognizer,
44
+ createTouchPress,
38
45
  dampWheelPan,
39
46
  defaultDragModifier,
40
47
  dwellDeadline,
41
48
  dwellReducer,
42
49
  isHeld,
43
50
  modHeld,
51
+ pagedSwipeDeadline,
52
+ pagedSwipeReducer,
44
53
  platformButton,
45
54
  resolveDragModifier,
55
+ swipeProgress,
46
56
  toModifiers,
57
+ touchPressDeadline,
58
+ touchPressReducer,
47
59
  wheelDeltaPx
48
- } from "./chunk-C4M62FUT.js";
60
+ } from "./chunk-KVFX5V4Y.js";
49
61
  import {
50
62
  batch,
51
63
  computed,
@@ -66,11 +78,16 @@ export {
66
78
  DWELL_DEFAULTS,
67
79
  DWELL_IDLE,
68
80
  NO_MODIFIERS,
81
+ PAGED_SWIPE_DEFAULTS,
82
+ PAGED_SWIPE_EMPTY_CONTEXT,
83
+ PAGED_SWIPE_IDLE,
69
84
  PHASES,
70
85
  RECOGNIZER_DEFAULTS,
71
86
  SETTLE_DEFAULTS,
72
87
  SETTLE_TEMPO,
73
88
  SPRING_DEFAULTS,
89
+ TOUCH_PRESS_DEFAULTS,
90
+ TOUCH_PRESS_IDLE,
74
91
  WHEEL_DEFAULTS,
75
92
  WHEEL_PAN_DEFAULTS,
76
93
  batch,
@@ -81,9 +98,11 @@ export {
81
98
  createFrameCoalescer,
82
99
  createFrameLoop,
83
100
  createMachine,
101
+ createPagedSwipe,
84
102
  createRecognizer,
85
103
  createSettle,
86
104
  createSpring,
105
+ createTouchPress,
87
106
  createTween,
88
107
  cubicBezier,
89
108
  dampWheelPan,
@@ -102,6 +121,8 @@ export {
102
121
  linear,
103
122
  modHeld,
104
123
  motionValue,
124
+ pagedSwipeDeadline,
125
+ pagedSwipeReducer,
105
126
  platformButton,
106
127
  popIn,
107
128
  resolveDragModifier,
@@ -112,7 +133,10 @@ export {
112
133
  signal,
113
134
  springStep,
114
135
  springValue,
136
+ swipeProgress,
115
137
  toModifiers,
138
+ touchPressDeadline,
139
+ touchPressReducer,
116
140
  tweenValue,
117
141
  untracked,
118
142
  wheelDeltaPx
package/dist/layout.d.ts CHANGED
@@ -58,6 +58,14 @@ interface LayoutItem {
58
58
  readonly shownSettled: boolean;
59
59
  /** Settle to `to` from where it is drawn; lays the content out at `to` and redraws at once. */
60
60
  moveTo(to: Rect): void;
61
+ /**
62
+ * Settle to `to` from where it is drawn, like {@link LayoutItem.moveTo}, but keep the content laid out in its box
63
+ * while `to` is the box's size: for chasing a moving point (a pointer), which would otherwise lay the content out
64
+ * again on every frame. A new size lays it out at `to`, as `moveTo` does. Optional, so an item from before it (or
65
+ * a test double) still works where it is used: {@link holdItem} falls back to `moveTo`. `createLayoutMotion`'s
66
+ * items always have it.
67
+ */
68
+ retarget?(to: Rect): void;
61
69
  /** Be at `to` now, at rest: no glide. */
62
70
  jumpTo(to: Rect): void;
63
71
  /** Drawn at `from` now, then settle to `to` (a FLIP's invert and play: `from` in the item's own coordinates). */
@@ -73,6 +81,11 @@ interface LayoutItem {
73
81
  }
74
82
  /** A task the layout motion runs on every frame before it steps the items; true keeps the frames coming. */
75
83
  type LayoutTask = (frame: FrameInfo) => boolean | void;
84
+ /** One item's frame in a batch ({@link LayoutMotionOptions.onDraw}). */
85
+ interface DrawEntry {
86
+ readonly id: string;
87
+ readonly frame: DrawFrame;
88
+ }
76
89
  interface LayoutMotionOptions {
77
90
  /** The frame loop every item steps on (one loop for all of them). */
78
91
  readonly loop: FrameLoop;
@@ -82,6 +95,20 @@ interface LayoutMotionOptions {
82
95
  readonly phase?: Phase;
83
96
  /** Where an appearing item grows from, as a share of its size ({@link popIn}'s `from`); default 0.87. */
84
97
  readonly popFrom?: number;
98
+ /**
99
+ * Reduced motion: read on every new target and every show or hide. While true an item is placed on its new
100
+ * target at once (no glide) and appears or disappears at once (no fade). A boolean, or the host's query (such as
101
+ * the reduced-motion media query). Default false.
102
+ */
103
+ readonly reducedMotion?: boolean | (() => boolean);
104
+ /**
105
+ * Batched drawing: every item that changed is drawn in one pass per frame, after the items step: each one's
106
+ * drawable, then this, once, with all of their frames. Changes made between frames (a new target, a hold) are
107
+ * drawn on the next frame, or now by {@link LayoutMotion.flush}. Without it, each item is drawn as it changes.
108
+ */
109
+ readonly onDraw?: (batch: readonly DrawEntry[]) => void;
110
+ /** Called when frames start running (true) and when they stop (false), once per change. */
111
+ readonly onActiveChange?: (active: boolean) => void;
85
112
  }
86
113
  /**
87
114
  * Layout motion: items with an id and a target rect, each settled to its place on one shared frame loop.
@@ -100,6 +127,14 @@ interface LayoutMotion {
100
127
  get(id: string): LayoutItem | undefined;
101
128
  /** Stop moving an item (its drawable is not touched again). */
102
129
  remove(id: string): void;
130
+ /** Remove every item whose id is not in `ids` (the ones that left the layout). */
131
+ retain(ids: Iterable<string>): void;
132
+ /**
133
+ * With {@link LayoutMotionOptions.onDraw}: draw the items changed since the last frame now, outside the loop. A
134
+ * host calls it right after it commits a new layout, so the next paint shows every item where it is drawn, not a
135
+ * frame at its new box. Without batching each item is already drawn as it changes, and it does nothing.
136
+ */
137
+ flush(): void;
103
138
  readonly items: ReadonlyMap<string, LayoutItem>;
104
139
  /** Run `task` on every frame, before the items step; returns how to stop it. */
105
140
  task(task: LayoutTask): () => void;
@@ -255,4 +290,4 @@ declare function drawBox(nodes: BoxNodes, frame: DrawFrame): void;
255
290
  /** A {@link Drawable} for DOM nodes: {@link layOutBox} on a new box, {@link drawBox} on every frame. */
256
291
  declare function domDrawable(nodes: BoxNodes): Drawable;
257
292
 
258
- export { type BoxNodes, type DrawFrame, type Drawable, type FlipEntry, type HeldDrag, type LayoutItem, type LayoutMotion, type LayoutMotionOptions, type LayoutTask, type Measurable, type RectLike, type StyleLike, type Styled, createLayoutMotion, domDrawable, drawBox, flip, holdItem, layOutBox, rectOf };
293
+ export { type BoxNodes, type DrawEntry, type DrawFrame, type Drawable, type FlipEntry, type HeldDrag, type LayoutItem, type LayoutMotion, type LayoutMotionOptions, type LayoutTask, type Measurable, type RectLike, type StyleLike, type Styled, createLayoutMotion, domDrawable, drawBox, flip, holdItem, layOutBox, rectOf };
package/dist/layout.js CHANGED
@@ -23,19 +23,45 @@ function createLayoutMotion(options) {
23
23
  let waiters = [];
24
24
  let stop = null;
25
25
  let woken = false;
26
+ let ticking = false;
27
+ let active = false;
28
+ const reduced = () => typeof options.reducedMotion === "function" ? options.reducedMotion() : options.reducedMotion === true;
29
+ const { onDraw, onActiveChange } = options;
30
+ const drawers = /* @__PURE__ */ new Map();
31
+ const dirty = /* @__PURE__ */ new Set();
32
+ const setActive = (on) => {
33
+ if (on === active) return;
34
+ active = on;
35
+ onActiveChange?.(on);
36
+ };
26
37
  const wake = () => {
27
38
  woken = true;
28
39
  if (stop) return;
29
40
  stop = loop.repeat(phase, tick);
41
+ setActive(true);
30
42
  };
43
+ function writeBatch() {
44
+ if (!onDraw || dirty.size === 0) return;
45
+ const batch = [];
46
+ for (const id of dirty) {
47
+ const entry = drawers.get(id)?.();
48
+ if (entry) batch.push(entry);
49
+ }
50
+ dirty.clear();
51
+ if (batch.length > 0) onDraw(batch);
52
+ }
31
53
  function tick(frame) {
32
54
  let moving = woken;
33
55
  woken = false;
56
+ ticking = true;
34
57
  for (const task of [...tasks]) if (task(frame)) moving = true;
35
58
  for (const step of [...steps.values()]) if (step(frame.delta)) moving = true;
59
+ writeBatch();
60
+ ticking = false;
36
61
  for (const listener of [...frames]) listener(frame);
37
62
  if (moving || woken) return true;
38
63
  stop = null;
64
+ setActive(false);
39
65
  const resolve = waiters;
40
66
  waiters = [];
41
67
  for (const r of resolve) r();
@@ -51,11 +77,33 @@ function createLayoutMotion(options) {
51
77
  const drawn = rect.value;
52
78
  return { drawn, box, reveal: reveal(drawn, box), pop: popIn(pop.value, popFrom), shown: pop.value, gone: pop.settled && pop.value === 0 };
53
79
  };
54
- const draw = () => drawable.draw(frameOf());
80
+ const drawNow = () => {
81
+ const frame = frameOf();
82
+ drawable.draw(frame);
83
+ return { id, frame };
84
+ };
85
+ const draw = () => {
86
+ if (!onDraw) {
87
+ drawNow();
88
+ return;
89
+ }
90
+ dirty.add(id);
91
+ if (!ticking) wake();
92
+ };
55
93
  const layOut = (to) => {
56
94
  box = copy(to);
57
95
  drawable.layOut(box);
58
96
  };
97
+ const aim = (to) => {
98
+ if (reduced()) {
99
+ rect.jumpTo(copy(to));
100
+ draw();
101
+ return;
102
+ }
103
+ rect.setTarget(copy(to));
104
+ wake();
105
+ };
106
+ let moved = false;
59
107
  const tempo = () => {
60
108
  rect.configure({ omega });
61
109
  pop.configure({ omega });
@@ -88,8 +136,15 @@ function createLayoutMotion(options) {
88
136
  layOut(to);
89
137
  draw();
90
138
  if (same(rect.target, to)) return;
91
- rect.setTarget(copy(to));
92
- wake();
139
+ aim(to);
140
+ },
141
+ retarget(to) {
142
+ if (to.width !== box.width || to.height !== box.height) {
143
+ item.moveTo(to);
144
+ return;
145
+ }
146
+ if (same(rect.target, to)) return;
147
+ aim(to);
93
148
  },
94
149
  jumpTo(to) {
95
150
  layOut(to);
@@ -100,8 +155,7 @@ function createLayoutMotion(options) {
100
155
  rect.jumpTo(copy(from));
101
156
  layOut(to);
102
157
  draw();
103
- rect.setTarget(copy(to));
104
- wake();
158
+ aim(to);
105
159
  },
106
160
  hold(r) {
107
161
  rect.hold(copy(r));
@@ -109,7 +163,7 @@ function createLayoutMotion(options) {
109
163
  },
110
164
  show(on, o = {}) {
111
165
  const want = on ? 1 : 0;
112
- if (o.now) pop.jumpTo(want);
166
+ if (o.now || reduced()) pop.jumpTo(want);
113
167
  else if (pop.target !== want) {
114
168
  pop.setTarget(want);
115
169
  wake();
@@ -118,6 +172,7 @@ function createLayoutMotion(options) {
118
172
  },
119
173
  redraw: draw
120
174
  };
175
+ drawers.set(id, drawNow);
121
176
  drawable.layOut(box);
122
177
  draw();
123
178
  items.set(id, item);
@@ -125,21 +180,32 @@ function createLayoutMotion(options) {
125
180
  steps.set(id, (dt) => {
126
181
  const a = rect.step(dt);
127
182
  const b = pop.step(dt);
128
- draw();
183
+ if (!onDraw || a || b || moved) draw();
184
+ moved = a || b;
129
185
  return a || b;
130
186
  });
131
- removers.set(id, () => tempos.delete(tempo));
187
+ removers.set(id, () => {
188
+ tempos.delete(tempo);
189
+ drawers.delete(id);
190
+ dirty.delete(id);
191
+ });
132
192
  return item;
133
193
  }
194
+ function remove(id) {
195
+ items.delete(id);
196
+ steps.delete(id);
197
+ removers.get(id)?.();
198
+ removers.delete(id);
199
+ }
134
200
  return {
135
201
  add,
136
202
  get: (id) => items.get(id),
137
- remove(id) {
138
- items.delete(id);
139
- steps.delete(id);
140
- removers.get(id)?.();
141
- removers.delete(id);
203
+ remove,
204
+ retain(ids) {
205
+ const keep = new Set(ids);
206
+ for (const id of [...items.keys()]) if (!keep.has(id)) remove(id);
142
207
  },
208
+ flush: writeBatch,
143
209
  items,
144
210
  task(task) {
145
211
  tasks.add(task);
@@ -166,8 +232,11 @@ function createLayoutMotion(options) {
166
232
  dispose() {
167
233
  stop?.();
168
234
  stop = null;
235
+ setActive(false);
169
236
  items.clear();
170
237
  steps.clear();
238
+ drawers.clear();
239
+ dirty.clear();
171
240
  tasks.clear();
172
241
  frames.clear();
173
242
  idles.clear();
@@ -206,7 +275,8 @@ function holdItem(item, grab) {
206
275
  const follow = () => {
207
276
  const goal = heldRect();
208
277
  if (!returning || !goal) return false;
209
- item.moveTo(goal);
278
+ if (item.retarget) item.retarget(goal);
279
+ else item.moveTo(goal);
210
280
  const d = item.drawn;
211
281
  if (Math.max(Math.abs(d.x - goal.x), Math.abs(d.y - goal.y), Math.abs(d.width - goal.width), Math.abs(d.height - goal.height)) >= CAUGHT) return false;
212
282
  returning = false;