@humanspeak/svelte-motion 0.5.4 → 0.6.1

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/dist/index.d.ts CHANGED
@@ -12,9 +12,10 @@ export { motion } from './motion';
12
12
  export { animate, delay, hover, inView, press, resize, scroll, stagger, transform } from 'motion';
13
13
  export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cubicBezier, easeIn, easeInOut, easeOut } from 'motion';
14
14
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
15
- export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
15
+ export type { AnimationControls, AnimationControlsDefinition, AnimationControlsSubscriber, DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionOnProjectionUpdate, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, ProjectionUpdatePayload, ReducedMotionConfig, Variants } from './types';
16
16
  export { useAnimate } from './utils/animate.svelte';
17
17
  export type { AnimationScope } from './utils/animate.svelte';
18
+ export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
18
19
  export { useAnimationFrame } from './utils/animationFrame';
19
20
  export type { AugmentedMotionValue } from './utils/augmentMotionValue.svelte';
20
21
  export { useCycle } from './utils/cycle.svelte';
@@ -29,6 +30,7 @@ export type { MotionTemplateInput } from './utils/motionTemplate.svelte';
29
30
  export { useMotionValue } from './utils/motionValue.svelte';
30
31
  export type { MotionValue, RawMotionValue } from './utils/motionValue.svelte';
31
32
  export { useMotionValueEvent } from './utils/motionValueEvent';
33
+ export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
32
34
  export { useReducedMotion } from './utils/reducedMotion.svelte';
33
35
  export type { ReducedMotionState } from './utils/reducedMotion.svelte';
34
36
  export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
15
15
  // Re-export utility functions
16
16
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
17
17
  export { useAnimate } from './utils/animate.svelte';
18
+ export { animationControls, isAnimationControls, useAnimation, useAnimationControls } from './utils/animationControls.svelte';
18
19
  export { useAnimationFrame } from './utils/animationFrame';
19
20
  export { useCycle } from './utils/cycle.svelte';
20
21
  export { createDragControls } from './utils/dragControls';
@@ -23,6 +24,7 @@ export { useInView } from './utils/inView.svelte';
23
24
  export { useMotionTemplate } from './utils/motionTemplate.svelte';
24
25
  export { useMotionValue } from './utils/motionValue.svelte';
25
26
  export { useMotionValueEvent } from './utils/motionValueEvent';
27
+ export { optimizedAppearDataAttribute, startOptimizedAppearAnimation } from './utils/optimizedAppear';
26
28
  export { useReducedMotion } from './utils/reducedMotion.svelte';
27
29
  export { useReducedMotionConfig } from './utils/reducedMotionConfig.svelte';
28
30
  export { useScroll } from './utils/scroll.svelte';
package/dist/types.d.ts CHANGED
@@ -74,7 +74,91 @@ export type MotionInitial = DOMKeyframesDefinition | string | string[] | false |
74
74
  * <motion.div variants={myVariants} animate="visible" />
75
75
  * ```
76
76
  */
77
- export type MotionAnimate = DOMKeyframesDefinition | string | string[] | undefined;
77
+ /**
78
+ * Definition accepted by legacy animation controls.
79
+ *
80
+ * Mirrors Motion's `AnimationDefinition`: a keyframes object, a variant
81
+ * label, an ordered list of variant labels, or a resolver function that
82
+ * receives `custom` data.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * controls.start('visible')
87
+ * controls.start(['visible', 'active'])
88
+ * controls.start({ opacity: 1, x: 0 })
89
+ * controls.start((custom) => ({ x: custom * 100 }))
90
+ * ```
91
+ */
92
+ export type AnimationControlsDefinition = DOMKeyframesDefinition | string | string[] | ((custom: unknown) => DOMKeyframesDefinition | string);
93
+ /**
94
+ * Internal subscriber shape used by {@link AnimationControls}.
95
+ *
96
+ * Motion's upstream controls subscribe VisualElements. Svelte Motion
97
+ * subscribes a lightweight adapter from each `motion.*` component.
98
+ */
99
+ export type AnimationControlsSubscriber = {
100
+ /** Start an animation on the subscribed component. */
101
+ start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown>;
102
+ /** Synchronously set final values on the subscribed component. */
103
+ set: (definition: AnimationControlsDefinition) => void;
104
+ /** Stop currently running animations on the subscribed component. */
105
+ stop: () => void;
106
+ };
107
+ /**
108
+ * Legacy imperative controls returned by {@link useAnimationControls}.
109
+ *
110
+ * Pass the object to `animate={controls}` on one or more `motion.*`
111
+ * components, then call `controls.start(...)`, `controls.set(...)`, or
112
+ * `controls.stop()` from events or effects.
113
+ *
114
+ * @example
115
+ * ```svelte
116
+ * <script lang="ts">
117
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
118
+ *
119
+ * const controls = useAnimationControls()
120
+ * </script>
121
+ *
122
+ * <button onclick={() => controls.start('open')}>Open</button>
123
+ * <motion.div animate={controls} variants={{ open: { opacity: 1 } }} />
124
+ * ```
125
+ */
126
+ export type AnimationControls = {
127
+ /**
128
+ * Subscribe a motion component adapter to these controls.
129
+ *
130
+ * @param subscriber Component adapter to animate.
131
+ * @returns Unsubscribe callback.
132
+ */
133
+ subscribe: (subscriber: AnimationControlsSubscriber) => () => void;
134
+ /**
135
+ * Start an animation on every subscribed component.
136
+ *
137
+ * @param definition Target keyframes, variant label(s), or resolver.
138
+ * @param transitionOverride Optional transition that overrides the
139
+ * component/default transition for this run.
140
+ * @returns Promise resolving when all subscribed animations complete.
141
+ */
142
+ start: (definition: AnimationControlsDefinition, transitionOverride?: AnimationOptions) => Promise<unknown[]>;
143
+ /**
144
+ * Synchronously set every subscribed component to the target's final
145
+ * values.
146
+ *
147
+ * @param definition Target keyframes, variant label(s), or resolver.
148
+ */
149
+ set: (definition: AnimationControlsDefinition) => void;
150
+ /** Stop animations on every subscribed component. */
151
+ stop: () => void;
152
+ /**
153
+ * Mark controls as mounted and return cleanup.
154
+ *
155
+ * Called automatically by `useAnimationControls()`.
156
+ *
157
+ * @returns Cleanup that marks controls unmounted and stops subscribers.
158
+ */
159
+ mount: () => () => void;
160
+ };
161
+ export type MotionAnimate = DOMKeyframesDefinition | string | string[] | AnimationControls | undefined;
78
162
  /**
79
163
  * Exit animation properties for a motion component when unmounted.
80
164
  *
@@ -431,8 +515,8 @@ export type MotionProps = {
431
515
  style?: string;
432
516
  /** CSS classes */
433
517
  class?: string;
434
- /** Enable FLIP layout animations; "position" limits to translation only */
435
- layout?: boolean | 'position';
518
+ /** Enable FLIP layout animations; string values select the upstream projection animation type. */
519
+ layout?: boolean | 'position' | 'size' | 'preserve-aspect';
436
520
  /**
437
521
  * Fires after each `layout`-driven change with the FLIP delta from
438
522
  * the element's internal projection node. Mirrors framer-motion's
@@ -0,0 +1,63 @@
1
+ import type { AnimationControls } from '../types.js';
2
+ /**
3
+ * Returns true when a value looks like Motion's legacy animation controls.
4
+ *
5
+ * Upstream `motion-dom` treats any non-null object with a `start`
6
+ * function as animation controls. Matching that narrow check keeps
7
+ * `animate={controls}` detection compatible with Motion's public shape.
8
+ *
9
+ * @param value Value passed to `animate`.
10
+ * @returns Whether `value` is an animation controls object.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const controls = useAnimationControls()
15
+ * isAnimationControls(controls) // true
16
+ * isAnimationControls({ opacity: 1 }) // false
17
+ * ```
18
+ */
19
+ export declare const isAnimationControls: (value: unknown) => value is AnimationControls;
20
+ /**
21
+ * Create legacy animation controls.
22
+ *
23
+ * This mirrors upstream Motion's `animationControls()`: controls collect
24
+ * subscribed motion components, guard `start`/`set` until mounted, fan out
25
+ * starts to every subscriber, and stop all subscribers on unmount.
26
+ *
27
+ * @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
28
+ * and `mount`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const controls = animationControls()
33
+ * const cleanup = controls.mount()
34
+ * await controls.start({ opacity: 1 })
35
+ * cleanup()
36
+ * ```
37
+ */
38
+ export declare const animationControls: () => AnimationControls;
39
+ /**
40
+ * Create imperative controls for one or more `motion.*` components.
41
+ *
42
+ * Pass the returned object to `animate={controls}`. Once mounted, call
43
+ * `controls.start(definition)`, `controls.set(definition)`, or
44
+ * `controls.stop()` to coordinate every subscribed component.
45
+ *
46
+ * @returns Mounted animation controls.
47
+ * @see https://motion.dev/docs/react-use-animation-controls
48
+ *
49
+ * @example
50
+ * ```svelte
51
+ * <script lang="ts">
52
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
53
+ *
54
+ * const controls = useAnimationControls()
55
+ * </script>
56
+ *
57
+ * <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
58
+ * <motion.div animate={controls} />
59
+ * ```
60
+ */
61
+ export declare const useAnimationControls: () => AnimationControls;
62
+ /** Alias matching Motion's legacy `useAnimation` export. */
63
+ export declare const useAnimation: () => AnimationControls;
@@ -0,0 +1,111 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
2
+ const mountedError = 'controls.start() should only be called after a component has mounted. Consider calling within a $effect.';
3
+ const setMountedError = 'controls.set() should only be called after a component has mounted. Consider calling within a $effect.';
4
+ /**
5
+ * Returns true when a value looks like Motion's legacy animation controls.
6
+ *
7
+ * Upstream `motion-dom` treats any non-null object with a `start`
8
+ * function as animation controls. Matching that narrow check keeps
9
+ * `animate={controls}` detection compatible with Motion's public shape.
10
+ *
11
+ * @param value Value passed to `animate`.
12
+ * @returns Whether `value` is an animation controls object.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const controls = useAnimationControls()
17
+ * isAnimationControls(controls) // true
18
+ * isAnimationControls({ opacity: 1 }) // false
19
+ * ```
20
+ */
21
+ export const isAnimationControls = (value) => {
22
+ return (value !== null &&
23
+ typeof value === 'object' &&
24
+ typeof value.start === 'function');
25
+ };
26
+ /**
27
+ * Create legacy animation controls.
28
+ *
29
+ * This mirrors upstream Motion's `animationControls()`: controls collect
30
+ * subscribed motion components, guard `start`/`set` until mounted, fan out
31
+ * starts to every subscriber, and stop all subscribers on unmount.
32
+ *
33
+ * @returns Animation controls with `subscribe`, `start`, `set`, `stop`,
34
+ * and `mount`.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const controls = animationControls()
39
+ * const cleanup = controls.mount()
40
+ * await controls.start({ opacity: 1 })
41
+ * cleanup()
42
+ * ```
43
+ */
44
+ export const animationControls = () => {
45
+ let hasMounted = false;
46
+ const subscribers = new SvelteSet();
47
+ const controls = {
48
+ subscribe(subscriber) {
49
+ subscribers.add(subscriber);
50
+ return () => {
51
+ subscribers.delete(subscriber);
52
+ };
53
+ },
54
+ start(definition, transitionOverride) {
55
+ if (!hasMounted) {
56
+ throw new Error(mountedError);
57
+ }
58
+ const animations = [];
59
+ subscribers.forEach((subscriber) => {
60
+ animations.push(subscriber.start(definition, transitionOverride));
61
+ });
62
+ return Promise.all(animations);
63
+ },
64
+ set(definition) {
65
+ if (!hasMounted) {
66
+ throw new Error(setMountedError);
67
+ }
68
+ subscribers.forEach((subscriber) => subscriber.set(definition));
69
+ },
70
+ stop() {
71
+ subscribers.forEach((subscriber) => subscriber.stop());
72
+ },
73
+ mount() {
74
+ hasMounted = true;
75
+ return () => {
76
+ hasMounted = false;
77
+ controls.stop();
78
+ };
79
+ }
80
+ };
81
+ return controls;
82
+ };
83
+ /**
84
+ * Create imperative controls for one or more `motion.*` components.
85
+ *
86
+ * Pass the returned object to `animate={controls}`. Once mounted, call
87
+ * `controls.start(definition)`, `controls.set(definition)`, or
88
+ * `controls.stop()` to coordinate every subscribed component.
89
+ *
90
+ * @returns Mounted animation controls.
91
+ * @see https://motion.dev/docs/react-use-animation-controls
92
+ *
93
+ * @example
94
+ * ```svelte
95
+ * <script lang="ts">
96
+ * import { motion, useAnimationControls } from '@humanspeak/svelte-motion'
97
+ *
98
+ * const controls = useAnimationControls()
99
+ * </script>
100
+ *
101
+ * <button onclick={() => controls.start({ scale: 1.2 })}>Pop</button>
102
+ * <motion.div animate={controls} />
103
+ * ```
104
+ */
105
+ export const useAnimationControls = () => {
106
+ const controls = animationControls();
107
+ $effect(() => controls.mount());
108
+ return controls;
109
+ };
110
+ /** Alias matching Motion's legacy `useAnimation` export. */
111
+ export const useAnimation = useAnimationControls;
@@ -6,11 +6,13 @@ import { type AnimationOptions } from 'motion';
6
6
  * immediately after reading the rect.
7
7
  *
8
8
  * When `scrollContainers` are provided, the returned rect is shifted by the
9
- * **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
10
- * computed from two such measures stay correct even when the user scrolls
11
- * any of the containers between measurements including a nested
12
- * `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
13
- * `removeElementScroll`, which walks every ancestor in the path.
9
+ * **sum** of each container's `scrollLeft` / `scrollTop`. When
10
+ * `includeViewportScroll` is true, the viewport's `window.scrollX` /
11
+ * `window.scrollY` is included too. FLIP deltas computed from two such
12
+ * measures stay correct even when the user scrolls between measurements —
13
+ * including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
14
+ * framer-motion's `removeElementScroll`, which walks every ancestor in the
15
+ * path, plus root scroll compensation from the projection tree.
14
16
  *
15
17
  * Pass an empty array (or omit) for viewport-relative behaviour.
16
18
  *
@@ -27,6 +29,7 @@ import { type AnimationOptions } from 'motion';
27
29
  * @param el Element to measure.
28
30
  * @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
29
31
  * @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
32
+ * @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
30
33
  * @returns DOMRect snapshot of the element.
31
34
  *
32
35
  * @example
@@ -41,7 +44,7 @@ import { type AnimationOptions } from 'motion';
41
44
  * const rect = measureRect(node, [innerScroll, outerScroll])
42
45
  * ```
43
46
  */
44
- export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string) => DOMRect;
47
+ export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string, includeViewportScroll?: boolean) => DOMRect;
45
48
  /**
46
49
  * Minimal rectangle shape `computeFlipTransforms` reads. A `DOMRect`
47
50
  * satisfies it structurally, and so does a projection `Box` converted to
@@ -1,4 +1,109 @@
1
1
  import { animate } from 'motion';
2
+ const layoutSizeAnimationAttribute = 'data-layout-size-animation';
3
+ const roundedPx = (value) => `${Math.max(0, Math.round(value))}px`;
4
+ const mix = (from, to, progress) => from + (to - from) * progress;
5
+ const isViewportOffscreen = (el) => {
6
+ if (typeof window === 'undefined')
7
+ return false;
8
+ const rect = el.getBoundingClientRect();
9
+ return (rect.bottom <= 0 ||
10
+ rect.right <= 0 ||
11
+ rect.top >= window.innerHeight ||
12
+ rect.left >= window.innerWidth);
13
+ };
14
+ const runBoxSizeAnimation = (el, transforms, transition) => {
15
+ const { dx, dy, sx, sy } = transforms;
16
+ const originalWidth = el.style.width;
17
+ const originalHeight = el.style.height;
18
+ const originalTransform = el.style.transform;
19
+ const originalTransformOrigin = el.style.transformOrigin;
20
+ const nextRect = el.getBoundingClientRect();
21
+ const prevWidth = nextRect.width * sx;
22
+ const prevHeight = nextRect.height * sy;
23
+ el.setAttribute(layoutSizeAnimationAttribute, 'true');
24
+ for (const child of el.querySelectorAll('[data-svelte-motion-layout]')) {
25
+ child.style.transform = '';
26
+ child.style.transformOrigin = '';
27
+ if (child.style.willChange === 'transform')
28
+ child.style.willChange = '';
29
+ }
30
+ el.style.width = roundedPx(prevWidth);
31
+ el.style.height = roundedPx(prevHeight);
32
+ const sizedRect = el.getBoundingClientRect();
33
+ const residualDx = nextRect.left + dx - sizedRect.left;
34
+ const residualDy = nextRect.top + dy - sizedRect.top;
35
+ const shouldTranslate = Math.abs(residualDx) > 0.5 || Math.abs(residualDy) > 0.5;
36
+ if (shouldTranslate) {
37
+ el.style.transformOrigin = '0 0';
38
+ el.style.transform = `translate(${Math.round(residualDx)}px, ${Math.round(residualDy)}px)`;
39
+ }
40
+ const writeBox = (progress) => {
41
+ el.style.width = roundedPx(mix(prevWidth, nextRect.width, progress));
42
+ el.style.height = roundedPx(mix(prevHeight, nextRect.height, progress));
43
+ if (shouldTranslate) {
44
+ const x = Math.round(mix(residualDx, 0, progress));
45
+ const y = Math.round(mix(residualDy, 0, progress));
46
+ el.style.transform = x === 0 && y === 0 ? '' : `translate(${x}px, ${y}px)`;
47
+ }
48
+ };
49
+ const animation = animate(0, 1, {
50
+ ...transition,
51
+ onUpdate: writeBox
52
+ });
53
+ let removeScrollListener;
54
+ let offscreenRaf = null;
55
+ let cleanupRan = false;
56
+ const cleanup = () => {
57
+ if (cleanupRan)
58
+ return;
59
+ cleanupRan = true;
60
+ removeScrollListener?.();
61
+ if (offscreenRaf !== null &&
62
+ typeof window !== 'undefined' &&
63
+ typeof window.cancelAnimationFrame === 'function') {
64
+ window.cancelAnimationFrame(offscreenRaf);
65
+ offscreenRaf = null;
66
+ }
67
+ el.style.width = originalWidth;
68
+ el.style.height = originalHeight;
69
+ el.style.transformOrigin = originalTransformOrigin;
70
+ el.style.transform = originalTransform;
71
+ el.removeAttribute(layoutSizeAnimationAttribute);
72
+ };
73
+ if (typeof window !== 'undefined') {
74
+ const completeIfOffscreen = () => {
75
+ if (cleanupRan)
76
+ return;
77
+ if (isViewportOffscreen(el)) {
78
+ animation.complete();
79
+ cleanup();
80
+ }
81
+ };
82
+ const scheduleCompleteIfOffscreen = () => {
83
+ if (typeof window.requestAnimationFrame !== 'function') {
84
+ completeIfOffscreen();
85
+ return;
86
+ }
87
+ if (offscreenRaf !== null)
88
+ return;
89
+ offscreenRaf = window.requestAnimationFrame(() => {
90
+ offscreenRaf = null;
91
+ completeIfOffscreen();
92
+ });
93
+ };
94
+ const handleScroll = () => {
95
+ completeIfOffscreen();
96
+ scheduleCompleteIfOffscreen();
97
+ };
98
+ window.addEventListener('scroll', handleScroll, { passive: true });
99
+ removeScrollListener = () => {
100
+ window.removeEventListener('scroll', handleScroll);
101
+ };
102
+ completeIfOffscreen();
103
+ scheduleCompleteIfOffscreen();
104
+ }
105
+ animation.finished?.finally(cleanup);
106
+ };
2
107
  /**
3
108
  * Measure an element's bounding client rect without current transform.
4
109
  *
@@ -6,11 +111,13 @@ import { animate } from 'motion';
6
111
  * immediately after reading the rect.
7
112
  *
8
113
  * When `scrollContainers` are provided, the returned rect is shifted by the
9
- * **sum** of each container's `scrollLeft` / `scrollTop`. FLIP deltas
10
- * computed from two such measures stay correct even when the user scrolls
11
- * any of the containers between measurements including a nested
12
- * `layoutScroll` inside another `layoutScroll`. Mirrors framer-motion's
13
- * `removeElementScroll`, which walks every ancestor in the path.
114
+ * **sum** of each container's `scrollLeft` / `scrollTop`. When
115
+ * `includeViewportScroll` is true, the viewport's `window.scrollX` /
116
+ * `window.scrollY` is included too. FLIP deltas computed from two such
117
+ * measures stay correct even when the user scrolls between measurements —
118
+ * including a nested `layoutScroll` inside another `layoutScroll`. Mirrors
119
+ * framer-motion's `removeElementScroll`, which walks every ancestor in the
120
+ * path, plus root scroll compensation from the projection tree.
14
121
  *
15
122
  * Pass an empty array (or omit) for viewport-relative behaviour.
16
123
  *
@@ -27,6 +134,7 @@ import { animate } from 'motion';
27
134
  * @param el Element to measure.
28
135
  * @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
29
136
  * @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
137
+ * @param includeViewportScroll Whether to include `window.scrollX/Y` in the returned rect.
30
138
  * @returns DOMRect snapshot of the element.
31
139
  *
32
140
  * @example
@@ -41,19 +149,22 @@ import { animate } from 'motion';
41
149
  * const rect = measureRect(node, [innerScroll, outerScroll])
42
150
  * ```
43
151
  */
44
- export const measureRect = (el, scrollContainers, baseTransform = 'none') => {
152
+ export const measureRect = (el, scrollContainers, baseTransform = 'none', includeViewportScroll = false) => {
45
153
  const prev = el.style.transform;
46
154
  try {
47
155
  el.style.transform = baseTransform;
48
156
  const rect = el.getBoundingClientRect();
49
- if (!scrollContainers || scrollContainers.length === 0)
50
- return rect;
157
+ let offsetLeft = includeViewportScroll && typeof window !== 'undefined' ? window.scrollX : 0;
158
+ let offsetTop = includeViewportScroll && typeof window !== 'undefined' ? window.scrollY : 0;
159
+ if (!scrollContainers || scrollContainers.length === 0) {
160
+ if (offsetLeft === 0 && offsetTop === 0)
161
+ return rect;
162
+ return new DOMRect(rect.left + offsetLeft, rect.top + offsetTop, rect.width, rect.height);
163
+ }
51
164
  // Re-express the rect in the *combined* scroll-container coordinate
52
165
  // space so a subsequent scroll on any of them doesn't show up as
53
166
  // movement. DOMRect's left/top are read-only, so allocate a fresh
54
167
  // one with the summed offsets applied.
55
- let offsetLeft = 0;
56
- let offsetTop = 0;
57
168
  for (const container of scrollContainers) {
58
169
  offsetLeft += container.scrollLeft;
59
170
  offsetTop += container.scrollTop;
@@ -95,6 +206,13 @@ export const runFlipAnimation = (el, transforms, transition) => {
95
206
  const { dx, dy, sx, sy, shouldTranslate, shouldScale } = transforms;
96
207
  if (!(shouldTranslate || shouldScale))
97
208
  return;
209
+ const correctionTargets = shouldScale
210
+ ? Array.from(el.querySelectorAll('[data-svelte-motion-layout]'))
211
+ : [];
212
+ if (shouldScale && correctionTargets.length > 0) {
213
+ runBoxSizeAnimation(el, { dx, dy, sx, sy }, transition);
214
+ return;
215
+ }
98
216
  const keyframes = {};
99
217
  if (shouldTranslate) {
100
218
  keyframes.x = [dx, 0];
@@ -140,6 +258,13 @@ export const observeLayoutChanges = (el, onChange) => {
140
258
  let pendingRaf = null;
141
259
  let releaseTimeout = null;
142
260
  const schedule = () => {
261
+ if (el.closest(`[${layoutSizeAnimationAttribute}]`)) {
262
+ el.style.transform = '';
263
+ el.style.transformOrigin = '';
264
+ if (el.style.willChange === 'transform')
265
+ el.style.willChange = '';
266
+ return;
267
+ }
143
268
  if (pendingRaf !== null || releaseTimeout !== null)
144
269
  return;
145
270
  // Leading-edge: call immediately, then throttle further calls until next frame (or 50ms)
@@ -157,14 +282,23 @@ export const observeLayoutChanges = (el, onChange) => {
157
282
  };
158
283
  const ro = new ResizeObserver(() => schedule());
159
284
  ro.observe(el);
160
- const mo = new MutationObserver(() => schedule());
161
- mo.observe(el, { attributes: true, attributeFilter: ['class', 'style'] });
285
+ const attributeObserver = new MutationObserver(() => schedule());
286
+ attributeObserver.observe(el, {
287
+ attributes: true,
288
+ attributeFilter: ['class', 'data-presence-layout-hold']
289
+ });
290
+ const childListObserver = new MutationObserver(() => schedule());
291
+ childListObserver.observe(el, {
292
+ childList: true,
293
+ subtree: true
294
+ });
162
295
  if (el.parentElement) {
163
- mo.observe(el.parentElement, { childList: true, subtree: false, attributes: true });
296
+ childListObserver.observe(el.parentElement, { childList: true, subtree: false });
164
297
  }
165
298
  return () => {
166
299
  ro.disconnect();
167
- mo.disconnect();
300
+ attributeObserver.disconnect();
301
+ childListObserver.disconnect();
168
302
  if (pendingRaf !== null && typeof cancelAnimationFrame === 'function') {
169
303
  cancelAnimationFrame(pendingRaf);
170
304
  pendingRaf = null;