@humanspeak/svelte-motion 0.1.24 → 0.1.25

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
@@ -44,7 +44,7 @@ Goal: Framer Motion API parity for Svelte where common React examples can be tra
44
44
  | Drag (`drag`, constraints, momentum, controls, callbacks) | Supported |
45
45
  | `AnimatePresence` (`initial`, `mode`, `onExitComplete`) | Supported |
46
46
  | Layout (`layout`, `layout="position"`) | Supported (single-element FLIP) |
47
- | Shared layout (`layoutId`) | Not yet supported |
47
+ | Shared layout (`layoutId`) | Supported |
48
48
  | Pan gesture API (`whilePan`, `onPan*`) | Not yet supported |
49
49
  | `MotionConfig` parity beyond `transition` | Partial |
50
50
  | `reducedMotion`, `features`, `transformPagePoint` | Not yet supported |
@@ -57,6 +57,7 @@
57
57
  isSVGTag,
58
58
  SVG_NAMESPACE
59
59
  } from '../utils/svg'
60
+ import { getLayoutIdRegistry } from '../utils/layoutId'
60
61
 
61
62
  type Props = MotionProps & {
62
63
  children?: Snippet
@@ -107,6 +108,7 @@
107
108
  dragListener: dragListenerProp,
108
109
  dragControls: dragControlsProp,
109
110
  layout: layoutProp,
111
+ layoutId: layoutIdProp,
110
112
  ref: element = $bindable(null),
111
113
  ...rest
112
114
  }: Props = $props()
@@ -117,6 +119,9 @@
117
119
  // Get presence context to check if we're inside AnimatePresence
118
120
  const context = getAnimatePresenceContext()
119
121
 
122
+ // Get layoutId registry (provided by AnimatePresence or a parent LayoutGroup)
123
+ const layoutIdRegistry = getLayoutIdRegistry()
124
+
120
125
  // Get current presence depth (0 = direct child of AnimatePresence, undefined = not in AnimatePresence)
121
126
  const presenceDepth = getPresenceDepth()
122
127
 
@@ -163,6 +168,36 @@
163
168
  })
164
169
  }
165
170
 
171
+ // Keep a live snapshot of the layoutId element's rect so the next element can FLIP from it.
172
+ // We store the last-known-good rect and push it to the registry on cleanup,
173
+ // because onDestroy fires after the element is removed from DOM (rect would be zeros).
174
+ let layoutIdLastRect: DOMRect | null = null
175
+ $effect(() => {
176
+ if (!(element && layoutIdProp && layoutIdRegistry)) return
177
+
178
+ // Capture rect on every frame while mounted
179
+ let rafId: number
180
+ const captureRect = () => {
181
+ if (element) {
182
+ layoutIdLastRect = element.getBoundingClientRect()
183
+ }
184
+ rafId = requestAnimationFrame(captureRect)
185
+ }
186
+ rafId = requestAnimationFrame(captureRect)
187
+
188
+ // On cleanup (before DOM removal), push last-known rect to registry
189
+ return () => {
190
+ cancelAnimationFrame(rafId)
191
+ if (layoutIdLastRect && layoutIdProp) {
192
+ layoutIdRegistry.snapshot(
193
+ layoutIdProp,
194
+ layoutIdLastRect,
195
+ (mergedTransition ?? {}) as AnimationOptions
196
+ )
197
+ }
198
+ }
199
+ })
200
+
166
201
  // Reactively update registration when element/exit/transition props change
167
202
  $effect(() => {
168
203
  if (element && context && resolvedExit) {
@@ -611,6 +646,25 @@
611
646
  }
612
647
  })
613
648
 
649
+ // Shared layout animation via layoutId.
650
+ // On mount, consume the previous snapshot and FLIP from its position.
651
+ $effect(() => {
652
+ if (!(element && layoutIdProp && layoutIdRegistry && isLoaded === 'ready')) return
653
+
654
+ const prev = layoutIdRegistry.consume(layoutIdProp)
655
+ if (!prev) return // First appearance, no animation needed
656
+
657
+ const next = measureRect(element)
658
+ const transforms = computeFlipTransforms(prev.rect, next, true)
659
+
660
+ setCompositorHints(element, true)
661
+ runFlipAnimation(
662
+ element,
663
+ transforms,
664
+ (prev.transition ?? mergedTransition ?? {}) as AnimationOptions
665
+ )
666
+ })
667
+
614
668
  // whileTap handling via motion-dom's press()
615
669
  $effect(() => {
616
670
  if (!(element && isLoaded === 'ready' && isNotEmpty(whileTapProp))) return
package/dist/types.d.ts CHANGED
@@ -278,6 +278,8 @@ export type MotionProps = {
278
278
  class?: string;
279
279
  /** Enable FLIP layout animations; "position" limits to translation only */
280
280
  layout?: boolean | 'position';
281
+ /** Shared layout animation identifier. Elements with matching layoutId animate between positions. */
282
+ layoutId?: string;
281
283
  /** Ref to the element */
282
284
  ref?: HTMLElement | null;
283
285
  /** Enable drag gestures. true for both axes, or lock to 'x'/'y'. */
@@ -0,0 +1,24 @@
1
+ import type { AnimationOptions } from 'motion';
2
+ /**
3
+ * Snapshot stored for a layoutId when an element unmounts.
4
+ */
5
+ type LayoutIdEntry = {
6
+ rect: DOMRect;
7
+ transition?: AnimationOptions;
8
+ };
9
+ /**
10
+ * Registry that stores last-known rect + transition for each layoutId.
11
+ *
12
+ * - `snapshot(id, rect, transition)` — called when an element with a layoutId unmounts.
13
+ * - `consume(id)` — called by a newly mounted element. Returns and **deletes** the entry (one-shot).
14
+ */
15
+ export type LayoutIdRegistry = {
16
+ snapshot(id: string, rect: DOMRect, transition?: AnimationOptions): void;
17
+ consume(id: string): LayoutIdEntry | undefined;
18
+ };
19
+ export declare const layoutIdRegistry: LayoutIdRegistry;
20
+ /**
21
+ * Get the global layoutId registry.
22
+ */
23
+ export declare function getLayoutIdRegistry(): LayoutIdRegistry;
24
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Module-level singleton registry shared across the entire component tree.
3
+ * This matches React Framer Motion's behavior where layoutId is shared globally
4
+ * (or within a LayoutGroup, which we can add later).
5
+ */
6
+ const entries = new Map();
7
+ export const layoutIdRegistry = {
8
+ snapshot(id, rect, transition) {
9
+ entries.set(id, { rect, transition });
10
+ },
11
+ consume(id) {
12
+ const entry = entries.get(id);
13
+ if (entry)
14
+ entries.delete(id);
15
+ return entry;
16
+ }
17
+ };
18
+ /**
19
+ * Get the global layoutId registry.
20
+ */
21
+ export function getLayoutIdRegistry() {
22
+ return layoutIdRegistry;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "A lightweight animation library for Svelte 5 that provides smooth, hardware-accelerated animations. Features include spring physics, custom easing, and fluid transitions. Built on top of the motion library, it offers a simple API for creating complex animations with minimal code. Perfect for interactive UIs, micro-interactions, and engaging user experiences.",
5
5
  "keywords": [
6
6
  "svelte",