@motion.page/sdk 1.0.2 → 1.0.4

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.
@@ -23,6 +23,8 @@ export declare class EventTrigger extends BaseTrigger<EventTriggerConfig> {
23
23
  private _listeners;
24
24
  private _isForward;
25
25
  private _frozenRects;
26
+ private _snapshotScrollX;
27
+ private _snapshotScrollY;
26
28
  private _wasHovering;
27
29
  private _hoverLeaveTimeout;
28
30
  private _boundMouseMoveHandler;
@@ -40,6 +42,8 @@ export declare class EventTrigger extends BaseTrigger<EventTriggerConfig> {
40
42
  * Called by TriggerManager.refreshScrollTriggers() on resize.
41
43
  */
42
44
  refresh(): void;
45
+ /** Snapshot frozen rects and the current scroll offset. */
46
+ private _snapshotRects;
43
47
  private _resolveTargets;
44
48
  private _addHoverListeners;
45
49
  private _handleLeaveAction;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GlobalScrollListener
3
+ *
4
+ * Consolidates per-instance scroll listeners into a single listener per scroller.
5
+ * This mirrors GSAP's architecture where one global scroll handler reads scrollY
6
+ * once and dispatches to all active ScrollTrigger instances in a single pass.
7
+ *
8
+ * Benefits:
9
+ * - Eliminates N scroll event listeners (one per ScrollTrigger) → 1 per scroller
10
+ * - For instant scrub: wraps all instance updates in a single runBatched() call,
11
+ * so DOM writes are flushed once instead of N times per scroll event
12
+ * - Reads scrollY once per scroll event, shared across all instances
13
+ *
14
+ * Architecture:
15
+ * - Each unique scroller (window or custom element) gets ONE passive scroll listener
16
+ * - On scroll, all registered callbacks for that scroller run inside a single
17
+ * runBatched() wrapper (batches instant-scrub DOM writes into one flush)
18
+ * - When the last callback for a scroller is removed, the listener is detached
19
+ */
20
+ type ScrollCallback = () => void;
21
+ /**
22
+ * Register a scroll callback for a given scroller.
23
+ * The first registration for a scroller attaches a single passive scroll listener.
24
+ */
25
+ export declare function addScrollCallback(scroller: EventTarget, callback: ScrollCallback): void;
26
+ /**
27
+ * Remove a scroll callback for a given scroller.
28
+ * When the last callback is removed, the scroll listener is detached.
29
+ */
30
+ export declare function removeScrollCallback(scroller: EventTarget, callback: ScrollCallback): void;
31
+ /**
32
+ * Reset all state (for testing only).
33
+ * @internal
34
+ */
35
+ export declare function _resetGlobalScrollListener(): void;
36
+ export {};
@@ -25,15 +25,29 @@ export declare class MarkerManager {
25
25
  private _scroller;
26
26
  private _startConfig;
27
27
  private _endConfig;
28
+ private _cachedElStartDocTop;
29
+ private _cachedElEndDocTop;
30
+ private _cachedElPositionValid;
28
31
  constructor(triggerId: number);
29
32
  /**
30
33
  * Create debug markers showing trigger positions
31
34
  */
32
35
  setup(config: MarkerSetupConfig): void;
33
36
  /**
34
- * Update marker positions on scroll
37
+ * Cache element marker positions in document/content coordinates.
38
+ * Called at setup and on refresh. This is the only place getLayoutRect() is called
39
+ * for element markers — the per-frame update() uses cached values instead.
40
+ */
41
+ private _cacheElementPositions;
42
+ /**
43
+ * Update marker positions on scroll.
44
+ * Uses cached document-relative positions to avoid getLayoutRect() per frame.
35
45
  */
36
46
  update(scrollTop: number, triggerElement: HTMLElement | null, viewportHeight: number): void;
47
+ /**
48
+ * Recache element positions after layout changes (resize, refresh).
49
+ */
50
+ recachePositions(viewportHeight: number): void;
37
51
  /**
38
52
  * Remove all markers
39
53
  */
@@ -39,6 +39,10 @@ export declare class PinManager {
39
39
  private _fixedTop;
40
40
  private _fixedLeft;
41
41
  private _width;
42
+ private _fixedBreakingAncestors;
43
+ private _isBodyPin;
44
+ private _originalHtmlHeight;
45
+ private _originalHtmlOverflow;
42
46
  constructor(id: number);
43
47
  /**
44
48
  * Setup pinning for an element
@@ -97,12 +101,74 @@ export declare class PinManager {
97
101
  bottom: number;
98
102
  right: number;
99
103
  } | null;
104
+ /**
105
+ * Whether this manager is using the body-pin strategy (html height extension).
106
+ */
107
+ isBodyPin(): boolean;
108
+ /**
109
+ * Temporarily reset html.style.height to its original (pre-pin) value so that
110
+ * callers can measure the body's natural height without the inflation caused
111
+ * by the pin spacing. Returns a restore function that re-applies the current
112
+ * (inflated) height. No-op for non-body pins.
113
+ *
114
+ * Used by ScrollTrigger.refresh() before _calculateTriggerPositions() so
115
+ * trigger positions are derived from the body's real content height.
116
+ */
117
+ suspendHtmlHeight(): (() => void) | null;
100
118
  /**
101
119
  * Check if using fixed pin strategy
102
120
  */
103
121
  isFixedPin(): boolean;
104
122
  /**
105
- * Resolve pinSpacing config to a concrete value
123
+ * Detect ancestor elements with CSS properties that break position:fixed.
124
+ *
125
+ * Any of these on an ancestor creates a new containing block, causing
126
+ * position:fixed to position relative to that ancestor instead of the
127
+ * viewport — making pinned elements scroll off-screen:
128
+ *
129
+ * - filter (even blur(0px)) — set by animation libraries as initial state
130
+ * - transform (even translate(0)) — set by smooth scroll libraries (Lenis)
131
+ * - will-change: transform/filter/perspective
132
+ * - perspective
133
+ * - contain: paint/layout/strict/content
134
+ * - backdrop-filter
135
+ * - overflow: clip — clips fixed descendants at element's padding box
136
+ *
137
+ * Only needs to run once during first setup (not on refresh).
138
+ */
139
+ private _detectFixedBreakingAncestors;
140
+ /**
141
+ * Override properties on ancestors that break position:fixed.
142
+ *
143
+ * IMPORTANT: Captures the current inline style value RIGHT BEFORE overriding,
144
+ * not at detection time. This prevents restoring stale values — e.g. if an
145
+ * animation set `filter: blur(5px)` at detection time but has since completed
146
+ * and the SDK cleaned it to `none`, we'd wrongly restore `blur(5px)` on unpin.
147
+ *
148
+ * Safe fallback values:
149
+ * - filter/transform/perspective → 'none'
150
+ * - will-change → 'auto'
151
+ * - contain → 'none'
152
+ * - overflow-x:clip → 'hidden' (still prevents horizontal scrollbar)
153
+ * - overflow-y:clip → 'visible'
154
+ */
155
+ private _overrideFixedBreakingAncestors;
156
+ /**
157
+ * Restore original values on ancestors that were overridden during pin.
158
+ * The values restored are the ones captured at override time (pin entry),
159
+ * not detection time — so they reflect the element's actual state just
160
+ * before the pin started.
161
+ */
162
+ private _restoreFixedBreakingAncestors;
163
+ /**
164
+ * Resolve pinSpacing config to a concrete value.
165
+ *
166
+ * Auto-detection rules (when pinSpacing is undefined or true):
167
+ * - Body pin: scroll room is handled via documentElement height, not a spacer.
168
+ * Return false so the (non-existent) spacer gets no extra padding/margin.
169
+ * - Flex parent: GSAP parity — a flex container reflows automatically around a
170
+ * fixed child via the spacer's dimensions; extra padding would double-count.
171
+ * - Everything else: default to 'padding' (adds pinDistance as padding-bottom).
106
172
  */
107
173
  private _resolvePinSpacing;
108
174
  /**
@@ -13,6 +13,8 @@ import type { Timeline } from '../core/Timeline';
13
13
  /** Config for page load trigger */
14
14
  export interface PageLoadTriggerConfig {
15
15
  timing?: 'before' | 'during' | 'after';
16
+ /** When true, the timeline is built but not played — use Motion('name').play() to start it manually */
17
+ paused?: boolean;
16
18
  }
17
19
  /** Interface that trigger instances must satisfy for cleanup */
18
20
  interface TriggerInstance {
@@ -18,6 +18,28 @@ export interface RepeatConfig {
18
18
  delay?: number;
19
19
  yoyo?: boolean;
20
20
  }
21
+ /**
22
+ * Configuration for a Timeline instance
23
+ */
24
+ export interface TimelineConfig {
25
+ /**
26
+ * Number of times to repeat the entire timeline.
27
+ * Use a number for simple repeats, or RepeatConfig for advanced options.
28
+ * Use -1 for infinite repeat.
29
+ * @example
30
+ * new Timeline('loop', { repeat: -1 })
31
+ * new Timeline('bounce', { repeat: { times: 3, delay: 0.2, yoyo: true } })
32
+ */
33
+ repeat?: number | RepeatConfig;
34
+ /** Called when the timeline starts playing (first frame with time > 0) */
35
+ onStart?: () => void;
36
+ /** Called every frame with the current progress (0–1) and time (seconds) */
37
+ onUpdate?: (progress: number, time: number) => void;
38
+ /** Called when the timeline completes all repeats */
39
+ onComplete?: () => void;
40
+ /** Called at the end of each repeat cycle */
41
+ onRepeat?: (repeatCount: number) => void;
42
+ }
21
43
  /**
22
44
  * Stagger configuration for multiple elements
23
45
  */
@@ -67,6 +89,7 @@ export interface AnimationVars {
67
89
  paddingBottom?: number | string;
68
90
  paddingLeft?: number | string;
69
91
  borderRadius?: number | string;
92
+ borderWidth?: number | string;
70
93
  fontSize?: number | string;
71
94
  lineHeight?: number | string;
72
95
  letterSpacing?: number | string;
@@ -26,7 +26,13 @@ export interface ParsedFilterFunction {
26
26
  */
27
27
  export declare function parseFilter(filter: string): ParsedFilterFunction[] | null;
28
28
  /**
29
- * Convert parsed filter functions back to CSS filter string
29
+ * Convert parsed filter functions back to CSS filter string.
30
+ *
31
+ * Returns 'none' when all filters are at their identity/default values
32
+ * (e.g. blur(0px), brightness(1)). This is critical because any filter
33
+ * value other than 'none' — even visually invisible ones like blur(0px) —
34
+ * creates a CSS containing block that breaks position:fixed on descendants.
35
+ * This would cause pinned sections to disappear during scroll animations.
30
36
  */
31
37
  export declare function filterToString(filters: ParsedFilterFunction[]): string;
32
38
  /**
@@ -41,6 +41,11 @@ export declare class TextSplitter {
41
41
  private static _splitWordsDom;
42
42
  /**
43
43
  * Split text nodes into character spans, preserving existing element wrappers.
44
+ *
45
+ * Characters are grouped by word inside implicit wrapper spans with
46
+ * `white-space: nowrap` so the browser never breaks a word across lines
47
+ * (matching GSAP SplitText behaviour). Only the char spans are exposed
48
+ * as animatable elements — the word wrappers are transparent to consumers.
44
49
  */
45
50
  private static _splitCharsDom;
46
51
  /**
@@ -62,6 +67,14 @@ export declare class TextSplitter {
62
67
  * The wrapper clips content so animating y:'100%' creates a reveal effect.
63
68
  */
64
69
  private static _applyMaskWrappers;
70
+ /**
71
+ * Detect if the split element uses `background-clip: text` (gradient text)
72
+ * and propagate the gradient through all generated spans so text stays visible.
73
+ *
74
+ * Without this, inline-block split spans inherit `-webkit-text-fill-color: transparent`
75
+ * but NOT the parent's gradient background, making the text invisible.
76
+ */
77
+ private static _propagateGradientText;
65
78
  /**
66
79
  * Revert element to original content
67
80
  */
@@ -11,5 +11,11 @@
11
11
  * coordinates from getBoundingClientRect(), which would produce incorrect
12
12
  * document-relative trigger positions. Temporarily clearing position/top/left/width
13
13
  * restores document-flow measurement before reading the rect.
14
+ *
15
+ * Performance notes:
16
+ * - The getComputedStyle() call to detect position:fixed is skipped when we
17
+ * can tell from the inline style alone (fast path for the common case).
18
+ * - No forced reflow after restoring styles — the browser batches style
19
+ * writes and applies them before the next paint automatically.
14
20
  */
15
21
  export declare function getLayoutRect(element: Element): DOMRect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion.page/sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "High-performance CSS animation SDK with scroll, hover, gesture, and cursor triggers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",