@humanspeak/svelte-motion 0.1.23 → 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 |
@@ -231,9 +231,11 @@ Single-element FLIP layout animation:
231
231
  ## Utilities
232
232
 
233
233
  - `useAnimationFrame`
234
+ - `useMotionTemplate`
234
235
  - `useSpring`
235
236
  - `useTime`
236
237
  - `useTransform`
238
+ - `useVelocity`
237
239
  - `styleString`
238
240
  - `stringifyStyleObject` (deprecated)
239
241
  - `createDragControls`
@@ -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/index.d.ts CHANGED
@@ -8,7 +8,9 @@ export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } f
8
8
  export type { DragAxis, DragConstraints, DragControls, DragInfo, DragTransition, MotionAnimate, MotionInitial, MotionOnDirectionLock, MotionOnDragTransitionEnd, MotionOnInViewEnd, MotionOnInViewStart, MotionTransition, MotionWhileDrag, MotionWhileFocus, MotionWhileHover, MotionWhileInView, MotionWhileTap, Variants } from './types';
9
9
  export { useAnimationFrame } from './utils/animationFrame';
10
10
  export { createDragControls } from './utils/dragControls';
11
+ export { useMotionTemplate } from './utils/motionTemplate';
11
12
  export { useSpring } from './utils/spring';
13
+ export { useVelocity } from './utils/velocity';
12
14
  /**
13
15
  * @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
14
16
  */
package/dist/index.js CHANGED
@@ -11,7 +11,9 @@ export { anticipate, backIn, backInOut, backOut, circIn, circInOut, circOut, cub
11
11
  export { clamp, distance, distance2D, interpolate, mix, pipe, progress, wrap } from 'motion';
12
12
  export { useAnimationFrame } from './utils/animationFrame';
13
13
  export { createDragControls } from './utils/dragControls';
14
+ export { useMotionTemplate } from './utils/motionTemplate';
14
15
  export { useSpring } from './utils/spring';
16
+ export { useVelocity } from './utils/velocity';
15
17
  /**
16
18
  * @deprecated Use `styleString` instead for reactive styles with automatic unit handling.
17
19
  */
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
+ }
@@ -0,0 +1,21 @@
1
+ import { type Readable } from 'svelte/store';
2
+ /**
3
+ * Tagged template literal that creates a derived store from interpolated
4
+ * readable stores. When any input store changes, the resulting store
5
+ * recomputes the template string.
6
+ *
7
+ * SSR-safe: `derived` from svelte/store works on the server.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const blur = useSpring(0)
12
+ * const filter = useMotionTemplate`blur(${blur}px)`
13
+ * // $filter → "blur(0px)"
14
+ * ```
15
+ *
16
+ * @param strings Static template string parts.
17
+ * @param values Readable stores to interpolate.
18
+ * @returns A readable store of the composed string.
19
+ * @see https://motion.dev/docs/react-use-motion-template
20
+ */
21
+ export declare const useMotionTemplate: (strings: TemplateStringsArray, ...values: Readable<number | string>[]) => Readable<string>;
@@ -0,0 +1,33 @@
1
+ import { derived } from 'svelte/store';
2
+ /**
3
+ * Tagged template literal that creates a derived store from interpolated
4
+ * readable stores. When any input store changes, the resulting store
5
+ * recomputes the template string.
6
+ *
7
+ * SSR-safe: `derived` from svelte/store works on the server.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const blur = useSpring(0)
12
+ * const filter = useMotionTemplate`blur(${blur}px)`
13
+ * // $filter → "blur(0px)"
14
+ * ```
15
+ *
16
+ * @param strings Static template string parts.
17
+ * @param values Readable stores to interpolate.
18
+ * @returns A readable store of the composed string.
19
+ * @see https://motion.dev/docs/react-use-motion-template
20
+ */
21
+ export const useMotionTemplate = (strings, ...values) => {
22
+ if (values.length === 0)
23
+ return derived([], () => strings[0] ?? '');
24
+ return derived(values, (current) => {
25
+ let result = '';
26
+ for (let i = 0; i < strings.length; i++) {
27
+ result += strings[i] ?? '';
28
+ if (i < current.length)
29
+ result += String(current[i]);
30
+ }
31
+ return result;
32
+ });
33
+ };
@@ -0,0 +1,15 @@
1
+ import { type Readable } from 'svelte/store';
2
+ /**
3
+ * Creates a readable store that tracks the velocity of a source store's value.
4
+ *
5
+ * Uses `motionValue` from motion-dom for built-in velocity tracking with
6
+ * timestamps. Polls velocity via `requestAnimationFrame` and settles to 0
7
+ * when movement stops.
8
+ *
9
+ * SSR-safe: returns a static `readable(0)` on the server.
10
+ *
11
+ * @param source A readable store of numeric or unit-string values.
12
+ * @returns A readable store of the current velocity in units/second.
13
+ * @see https://motion.dev/docs/react-use-velocity
14
+ */
15
+ export declare const useVelocity: (source: Readable<number | string>) => Readable<number>;
@@ -0,0 +1,62 @@
1
+ import { motionValue } from 'motion-dom';
2
+ import { readable, writable } from 'svelte/store';
3
+ /**
4
+ * Parses a numeric value from a number or unit string (e.g. "100px" → 100).
5
+ */
6
+ const parseNumeric = (v) => {
7
+ if (typeof v === 'number')
8
+ return v;
9
+ const parsed = Number.parseFloat(String(v));
10
+ return Number.isFinite(parsed) ? parsed : 0;
11
+ };
12
+ /**
13
+ * Creates a readable store that tracks the velocity of a source store's value.
14
+ *
15
+ * Uses `motionValue` from motion-dom for built-in velocity tracking with
16
+ * timestamps. Polls velocity via `requestAnimationFrame` and settles to 0
17
+ * when movement stops.
18
+ *
19
+ * SSR-safe: returns a static `readable(0)` on the server.
20
+ *
21
+ * @param source A readable store of numeric or unit-string values.
22
+ * @returns A readable store of the current velocity in units/second.
23
+ * @see https://motion.dev/docs/react-use-velocity
24
+ */
25
+ export const useVelocity = (source) => {
26
+ if (typeof window === 'undefined')
27
+ return readable(0, () => { });
28
+ const mv = motionValue(0);
29
+ const store = writable(0);
30
+ let raf = 0;
31
+ let settled = true;
32
+ const poll = () => {
33
+ const v = mv.getVelocity();
34
+ store.set(v);
35
+ if (Math.abs(v) < 0.001) {
36
+ settled = true;
37
+ store.set(0);
38
+ raf = 0;
39
+ return;
40
+ }
41
+ raf = requestAnimationFrame(poll);
42
+ };
43
+ const startPolling = () => {
44
+ if (!settled)
45
+ return;
46
+ settled = false;
47
+ raf = requestAnimationFrame(poll);
48
+ };
49
+ return readable(0, (set) => {
50
+ const unsubStore = store.subscribe(set);
51
+ const unsubSource = source.subscribe((v) => {
52
+ mv.set(parseNumeric(v));
53
+ startPolling();
54
+ });
55
+ return () => {
56
+ unsubStore();
57
+ unsubSource();
58
+ if (raf)
59
+ cancelAnimationFrame(raf);
60
+ };
61
+ });
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.1.23",
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",
@@ -111,7 +111,7 @@
111
111
  "svelte": "^5.0.0"
112
112
  },
113
113
  "volta": {
114
- "node": "24.12.0"
114
+ "node": "22.16.0"
115
115
  },
116
116
  "publishConfig": {
117
117
  "access": "public"