@humanspeak/svelte-motion 0.4.2 → 0.4.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.
package/README.md CHANGED
@@ -214,6 +214,29 @@ Supported drag props:
214
214
 
215
215
  - String variant keys are resolved from `variants`.
216
216
  - Variant state inherits through context.
217
+ - A variant entry can be a `(custom) => keyframes` factory. The `custom` prop is forwarded — useful for staggered lists where each child needs its own offset or delay. Children without `custom` inherit the nearest motion ancestor's value.
218
+
219
+ ```svelte
220
+ <script lang="ts">
221
+ import { motion, type Variants } from '@humanspeak/svelte-motion'
222
+
223
+ const variants: Variants = {
224
+ hidden: { opacity: 0, x: -100 },
225
+ visible: (i) => ({
226
+ opacity: 1,
227
+ x: 0,
228
+ transition: { delay: (i as number) * 0.1 }
229
+ })
230
+ }
231
+ const items = ['Alpha', 'Beta', 'Gamma', 'Delta']
232
+ </script>
233
+
234
+ {#each items as item, i}
235
+ <motion.li custom={i} {variants} initial="hidden" animate="visible">
236
+ {item}
237
+ </motion.li>
238
+ {/each}
239
+ ```
217
240
 
218
241
  ## Layout animation
219
242
 
@@ -25,3 +25,23 @@ export declare const setInitialFalseContext: (value: boolean) => void;
25
25
  * @returns `true` if a parent set `initial={false}`, otherwise `false`.
26
26
  */
27
27
  export declare const getInitialFalseContext: () => boolean;
28
+ /**
29
+ * Provide a writable store carrying the current motion component's
30
+ * `custom` value so descendant motion components without their own
31
+ * `custom` prop can inherit it — and re-resolve their variants when the
32
+ * parent's `custom` changes.
33
+ *
34
+ * Mirrors framer-motion's variant-tree custom propagation. A store
35
+ * (rather than a snapshot) lets descendants react to changes the parent
36
+ * makes after mount.
37
+ *
38
+ * @param store Writable store holding the current component's effective `custom`.
39
+ */
40
+ export declare const setCustomContext: (store: Writable<unknown>) => void;
41
+ /**
42
+ * Read the nearest ancestor's `custom` store (if any).
43
+ *
44
+ * @returns The ancestor's writable store, or `undefined` when no motion
45
+ * ancestor has set one.
46
+ */
47
+ export declare const getCustomContext: () => Writable<unknown> | undefined;
@@ -1,6 +1,7 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  const VARIANT_CONTEXT_KEY = Symbol('variant-context');
3
3
  const INITIAL_FALSE_CONTEXT_KEY = Symbol('initial-false-context');
4
+ const CUSTOM_CONTEXT_KEY = Symbol('custom-context');
4
5
  /**
5
6
  * Provide a writable store for the current variant key so children can
6
7
  * react to changes over time (true inheritance like Framer Motion).
@@ -35,3 +36,27 @@ export const setInitialFalseContext = (value) => {
35
36
  export const getInitialFalseContext = () => {
36
37
  return getContext(INITIAL_FALSE_CONTEXT_KEY) ?? false;
37
38
  };
39
+ /**
40
+ * Provide a writable store carrying the current motion component's
41
+ * `custom` value so descendant motion components without their own
42
+ * `custom` prop can inherit it — and re-resolve their variants when the
43
+ * parent's `custom` changes.
44
+ *
45
+ * Mirrors framer-motion's variant-tree custom propagation. A store
46
+ * (rather than a snapshot) lets descendants react to changes the parent
47
+ * makes after mount.
48
+ *
49
+ * @param store Writable store holding the current component's effective `custom`.
50
+ */
51
+ export const setCustomContext = (store) => {
52
+ setContext(CUSTOM_CONTEXT_KEY, store);
53
+ };
54
+ /**
55
+ * Read the nearest ancestor's `custom` store (if any).
56
+ *
57
+ * @returns The ancestor's writable store, or `undefined` when no motion
58
+ * ancestor has set one.
59
+ */
60
+ export const getCustomContext = () => {
61
+ return getContext(CUSTOM_CONTEXT_KEY);
62
+ };
@@ -53,7 +53,9 @@
53
53
  setVariantContext,
54
54
  getVariantContext,
55
55
  setInitialFalseContext,
56
- getInitialFalseContext
56
+ getInitialFalseContext,
57
+ setCustomContext,
58
+ getCustomContext
57
59
  } from '../components/variantContext.context'
58
60
  import { get, writable } from 'svelte/store'
59
61
  import {
@@ -75,6 +77,7 @@
75
77
  tag = 'div',
76
78
  key: keyProp,
77
79
  variants: variantsProp,
80
+ custom: customProp,
78
81
  initial: initialProp,
79
82
  animate: animateProp,
80
83
  exit: exitProp,
@@ -87,6 +90,7 @@
87
90
  whileHover: whileHoverProp,
88
91
  whileFocus: whileFocusProp,
89
92
  whileInView: whileInViewProp,
93
+ viewport: viewportProp,
90
94
  whileDrag: whileDragProp,
91
95
  onHoverStart: onHoverStartProp,
92
96
  onHoverEnd: onHoverEndProp,
@@ -359,6 +363,32 @@
359
363
  // Provide context immediately during initialization so children can inherit
360
364
  setVariantContext(localVariantStore)
361
365
 
366
+ // Custom-value inheritance. Children with no `custom` prop adopt the
367
+ // nearest motion ancestor's value. Reactive via a writable store so a
368
+ // parent updating `custom` re-fires descendants' variant resolution.
369
+ const parentCustomStore = getCustomContext()
370
+ let inheritedCustom: unknown = undefined
371
+ if (parentCustomStore) {
372
+ parentCustomStore.subscribe((v) => (inheritedCustom = v))()
373
+ }
374
+ const initialCustomValue = customProp !== undefined ? customProp : inheritedCustom
375
+ const localCustomStore = writable<unknown>(initialCustomValue)
376
+ setCustomContext(localCustomStore)
377
+
378
+ let parentInheritedCustom = $state<unknown>(inheritedCustom)
379
+ $effect(() => {
380
+ if (!parentCustomStore) {
381
+ parentInheritedCustom = undefined
382
+ return
383
+ }
384
+ const unsubscribe = parentCustomStore.subscribe((v) => (parentInheritedCustom = v))
385
+ return () => unsubscribe()
386
+ })
387
+ const effectiveCustom = $derived(customProp !== undefined ? customProp : parentInheritedCustom)
388
+ $effect(() => {
389
+ localCustomStore.set(effectiveCustom)
390
+ })
391
+
362
392
  $effect(() => {
363
393
  if (!variantsProp) return localVariantStore.set(undefined)
364
394
  if (typeof animateProp === 'string') return localVariantStore.set(animateProp)
@@ -366,9 +396,13 @@
366
396
  localVariantStore.set(undefined)
367
397
  })
368
398
 
369
- const resolvedInitial = $derived(resolveInitial(effectiveInitialProp, variantsProp))
370
- const resolvedAnimate = $derived(resolveAnimate(effectiveAnimate, variantsProp))
371
- const resolvedExit = $derived(resolveExit(exitProp, variantsProp))
399
+ const resolvedInitial = $derived(
400
+ resolveInitial(effectiveInitialProp, variantsProp, effectiveCustom)
401
+ )
402
+ const resolvedAnimate = $derived(
403
+ resolveAnimate(effectiveAnimate, variantsProp, effectiveCustom)
404
+ )
405
+ const resolvedExit = $derived(resolveExit(exitProp, variantsProp, effectiveCustom))
372
406
 
373
407
  // Extract keyframes from resolved initial, handling initial={false}
374
408
  const initialKeyframes = $derived(
@@ -638,6 +672,12 @@
638
672
 
639
673
  // Track the last variant key we ran to avoid re-running on mount
640
674
  let lastRanVariantKey = $state<string | undefined>(undefined)
675
+ // Companion to `lastRanVariantKey`: the JSON-serialized resolved
676
+ // keyframes for that variant. Lets us detect when a function-form
677
+ // variant produces new keyframes (because `custom` changed) while
678
+ // the variant key stayed the same — otherwise the animate effect
679
+ // would short-circuit and the element would never re-animate.
680
+ let lastRanResolvedJson = $state<string | undefined>(undefined)
641
681
  let mountedWithInitialFalse = $state(false)
642
682
  // Track if the initial->animate transition has already been triggered by main effect
643
683
  let initialAnimationTriggered = $state(false)
@@ -781,7 +821,8 @@
781
821
  {
782
822
  initial: (resolvedInitial ?? {}) as Record<string, unknown>,
783
823
  animate: (resolvedAnimate ?? {}) as Record<string, unknown>
784
- }
824
+ },
825
+ viewportProp
785
826
  )
786
827
  })
787
828
 
@@ -914,8 +955,14 @@
914
955
  return
915
956
  }
916
957
  if (typeof animateProp === 'string') {
917
- if (lastRanVariantKey !== animateProp) {
958
+ // Compare BOTH the variant key and the resolved keyframes JSON.
959
+ // For static variants the JSON is constant per key; for
960
+ // function-form variants the JSON changes when `custom`
961
+ // changes, which we must treat as a new animation target.
962
+ const resolvedJson = resolvedAnimate ? JSON.stringify(resolvedAnimate) : undefined
963
+ if (lastRanVariantKey !== animateProp || lastRanResolvedJson !== resolvedJson) {
918
964
  lastRanVariantKey = animateProp
965
+ lastRanResolvedJson = resolvedJson
919
966
  runAnimation()
920
967
  }
921
968
  } else if (animateProp) {
@@ -954,8 +1001,10 @@
954
1001
  mountedWithInitialFalse = false
955
1002
  }
956
1003
  if (typeof currentAnimateKey === 'string') {
957
- if (lastRanVariantKey !== currentAnimateKey) {
1004
+ const resolvedJson = resolvedAnimate ? JSON.stringify(resolvedAnimate) : undefined
1005
+ if (lastRanVariantKey !== currentAnimateKey || lastRanResolvedJson !== resolvedJson) {
958
1006
  lastRanVariantKey = currentAnimateKey
1007
+ lastRanResolvedJson = resolvedJson
959
1008
  runAnimation()
960
1009
  }
961
1010
  } else {
@@ -987,6 +1036,9 @@
987
1036
  mountedWithInitialFalse = true
988
1037
  if (typeof currentAnimateKey === 'string') {
989
1038
  lastRanVariantKey = currentAnimateKey
1039
+ lastRanResolvedJson = resolvedAnimate
1040
+ ? JSON.stringify(resolvedAnimate)
1041
+ : undefined
990
1042
  }
991
1043
  dataPath = 5
992
1044
  isLoaded = 'ready'
@@ -1079,6 +1131,9 @@
1079
1131
  snapshot = transformSVGPathProperties(element!, snapshot)
1080
1132
  animate(element!, snapshot as DOMKeyframesDefinition, { duration: 0 })
1081
1133
  lastRanVariantKey = currentAnimateKey
1134
+ lastRanResolvedJson = resolvedAnimate
1135
+ ? JSON.stringify(resolvedAnimate)
1136
+ : undefined
1082
1137
  } else {
1083
1138
  runAnimation()
1084
1139
  }
package/dist/types.d.ts CHANGED
@@ -1,8 +1,32 @@
1
1
  import type { AnimationOptions, DOMKeyframesDefinition } from 'motion';
2
2
  import type { Snippet } from 'svelte';
3
+ /**
4
+ * A variant value: either a static keyframes object, or a factory function
5
+ * that receives the consumer-provided `custom` value and returns keyframes.
6
+ *
7
+ * Dynamic (function-form) variants let a single variants object emit
8
+ * per-instance keyframes — common for staggered lists where each child
9
+ * needs its own offset or delay.
10
+ *
11
+ * @example
12
+ * ```svelte
13
+ * <motion.div
14
+ * custom={index}
15
+ * variants={{
16
+ * visible: (i) => ({ opacity: 1, x: i * 50 }),
17
+ * hidden: { opacity: 0 }
18
+ * }}
19
+ * animate="visible"
20
+ * />
21
+ * ```
22
+ */
23
+ export type Variant = DOMKeyframesDefinition | ((custom: unknown) => DOMKeyframesDefinition) | undefined;
3
24
  /**
4
25
  * Variants define named animation states that can be referenced by string keys.
5
26
  *
27
+ * Each entry can be a static keyframes object or a `(custom) => keyframes`
28
+ * factory function (see {@link Variant}).
29
+ *
6
30
  * @example
7
31
  * ```svelte
8
32
  * <script>
@@ -15,7 +39,7 @@ import type { Snippet } from 'svelte';
15
39
  * <motion.div variants={variants} animate="open" />
16
40
  * ```
17
41
  */
18
- export type Variants = Record<string, DOMKeyframesDefinition | undefined>;
42
+ export type Variants = Record<string, Variant>;
19
43
  /**
20
44
  * Initial animation properties for a motion component.
21
45
  *
@@ -133,6 +157,29 @@ export type MotionWhileDrag = (Record<string, unknown> & {
133
157
  export type MotionWhileInView = (Record<string, unknown> & {
134
158
  transition?: AnimationOptions;
135
159
  }) | DOMKeyframesDefinition | undefined;
160
+ /**
161
+ * IntersectionObserver configuration for `whileInView`. Mirrors framer-motion's
162
+ * `viewport` prop. Same shape as `UseInViewOptions` minus `initial` (which is
163
+ * only meaningful for the hook's pre-mount return value).
164
+ *
165
+ * @example
166
+ * ```svelte
167
+ * <motion.div
168
+ * whileInView={{ opacity: 1, y: 0 }}
169
+ * viewport={{ once: true, amount: 0.5 }}
170
+ * />
171
+ * ```
172
+ */
173
+ export type MotionViewport = {
174
+ /** When `true`, fire only once on first entry. Subsequent re-entries no-op. */
175
+ once?: boolean;
176
+ /** Element to use as the IntersectionObserver root. Defaults to the viewport. */
177
+ root?: Element | Document;
178
+ /** CSS margin string applied to the root bounding box (e.g. `"100px 0px"`). */
179
+ margin?: string;
180
+ /** Fraction (0-1) or `"some"` / `"all"` of the target that must be visible. */
181
+ amount?: 'some' | 'all' | number;
182
+ };
136
183
  /**
137
184
  * Animation transition configuration for hover interactions.
138
185
  * Overrides the global transition when provided.
@@ -222,6 +269,12 @@ export type MotionProps = {
222
269
  key?: string;
223
270
  /** Variants define named animation states */
224
271
  variants?: Variants;
272
+ /**
273
+ * Value passed into function-form variants. Children without their own
274
+ * `custom` prop inherit this from the nearest motion ancestor — matching
275
+ * framer-motion's variant-tree custom propagation.
276
+ */
277
+ custom?: unknown;
225
278
  /** Initial state of the animation (object or variant key) */
226
279
  initial?: MotionInitial;
227
280
  /** Target state of the animation (object or variant key) */
@@ -240,6 +293,8 @@ export type MotionProps = {
240
293
  whileDrag?: MotionWhileDrag;
241
294
  /** In-view interaction animation - animates when element enters viewport */
242
295
  whileInView?: MotionWhileInView;
296
+ /** IntersectionObserver options for `whileInView` (once / root / margin / amount) */
297
+ viewport?: MotionViewport;
243
298
  /** Called right before a main animate transition starts */
244
299
  onAnimationStart?: MotionAnimationStart;
245
300
  /** Called after a main animate transition completes */
@@ -1,5 +1,6 @@
1
1
  import { type AnimationOptions, type DOMKeyframesDefinition } from 'motion';
2
2
  import { type Readable } from 'svelte/store';
3
+ import type { MotionViewport } from '../types.js';
3
4
  import { type ElementOrGetter } from './dom.js';
4
5
  /**
5
6
  * Split a whileInView definition into keyframes and an optional nested transition.
@@ -57,6 +58,7 @@ export declare const computeInViewBaseline: (el: HTMLElement, opts: {
57
58
  * @param mergedTransition Root/component merged transition.
58
59
  * @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
59
60
  * @param baselineSources Optional sources used to compute baseline.
61
+ * @param viewport Optional IntersectionObserver options. `amount` defaults to `0` (any pixel visible).
60
62
  * @returns Cleanup function to stop observing.
61
63
  * @example
62
64
  * const cleanup = attachWhileInView(
@@ -67,7 +69,8 @@ export declare const computeInViewBaseline: (el: HTMLElement, opts: {
67
69
  * onStart: () => console.log('Entered viewport'),
68
70
  * onEnd: () => console.log('Left viewport')
69
71
  * },
70
- * { initial: { opacity: 0, y: 50 } }
72
+ * { initial: { opacity: 0, y: 50 } },
73
+ * { once: true, amount: 0.5 }
71
74
  * )
72
75
  * // Later: cleanup() to stop observing
73
76
  */
@@ -78,7 +81,7 @@ export declare const attachWhileInView: (el: HTMLElement, whileInView: Record<st
78
81
  }, baselineSources?: {
79
82
  initial?: Record<string, unknown>;
80
83
  animate?: Record<string, unknown>;
81
- }) => (() => void);
84
+ }, viewport?: MotionViewport) => (() => void);
82
85
  /**
83
86
  * Options accepted by `useInView`.
84
87
  */
@@ -122,6 +122,7 @@ export const computeInViewBaseline = (el, opts) => {
122
122
  * @param mergedTransition Root/component merged transition.
123
123
  * @param callbacks Optional lifecycle callbacks for in-view start/end and animation completion.
124
124
  * @param baselineSources Optional sources used to compute baseline.
125
+ * @param viewport Optional IntersectionObserver options. `amount` defaults to `0` (any pixel visible).
125
126
  * @returns Cleanup function to stop observing.
126
127
  * @example
127
128
  * const cleanup = attachWhileInView(
@@ -132,14 +133,18 @@ export const computeInViewBaseline = (el, opts) => {
132
133
  * onStart: () => console.log('Entered viewport'),
133
134
  * onEnd: () => console.log('Left viewport')
134
135
  * },
135
- * { initial: { opacity: 0, y: 50 } }
136
+ * { initial: { opacity: 0, y: 50 } },
137
+ * { once: true, amount: 0.5 }
136
138
  * )
137
139
  * // Later: cleanup() to stop observing
138
140
  */
139
- export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources) => {
141
+ export const attachWhileInView = (el, whileInView, mergedTransition, callbacks, baselineSources, viewport) => {
140
142
  if (!whileInView)
141
143
  return () => { };
142
- return motionInView(el, () => {
144
+ let latched = false;
145
+ const stop = motionInView(el, () => {
146
+ if (latched)
147
+ return;
143
148
  const inViewBaseline = computeInViewBaseline(el, {
144
149
  initial: baselineSources?.initial,
145
150
  animate: baselineSources?.animate,
@@ -155,13 +160,28 @@ export const attachWhileInView = (el, whileInView, mergedTransition, callbacks,
155
160
  .catch(() => {
156
161
  /* animation cancelled — skip completion callback */
157
162
  });
163
+ if (viewport?.once) {
164
+ // Latch on first entry. Don't return an exit handler so the
165
+ // element holds its in-view state and we never animate back.
166
+ // Stop observing entirely after the entry handler returns.
167
+ latched = true;
168
+ queueMicrotask(stop);
169
+ return;
170
+ }
158
171
  return () => {
159
172
  if (Object.keys(inViewBaseline).length > 0) {
160
173
  animate(el, inViewBaseline, mergedTransition);
161
174
  }
162
175
  callbacks?.onEnd?.();
163
176
  };
164
- }, { amount: 0 });
177
+ }, {
178
+ root: viewport?.root,
179
+ // framer-motion types `margin` as a CSS-shorthand template literal;
180
+ // we expose plain `string` so consumers can pass any computed value.
181
+ margin: viewport?.margin,
182
+ amount: viewport?.amount ?? 0
183
+ });
184
+ return stop;
165
185
  };
166
186
  /**
167
187
  * Returns a Svelte readable store that tracks whether `target` is in the
@@ -3,80 +3,74 @@ import type { DOMKeyframesDefinition } from 'motion';
3
3
  /**
4
4
  * Resolves a variant key to its keyframes definition.
5
5
  *
6
- * Looks up a variant name in the variants object and returns the corresponding
7
- * keyframes. Returns `undefined` if the variants object or key is not provided.
6
+ * Looks up `key` in `variants`. When the entry is a function (dynamic
7
+ * variant), it's invoked with `custom` to produce keyframes matching
8
+ * framer-motion's per-instance variant pattern.
8
9
  *
9
10
  * @param variants - The variants object containing named animation states.
10
11
  * @param key - The variant key to look up.
11
- * @returns The keyframes definition for the variant, or undefined if not found.
12
+ * @param custom - Value forwarded to function-form variants. Pass-through
13
+ * `undefined` when no `custom` is in scope; the dynamic variant itself
14
+ * decides how to handle the absent input.
15
+ * @returns The keyframes definition for the variant, or `undefined` if the
16
+ * key is missing.
12
17
  *
13
18
  * @example
14
- * ```typescript
19
+ * ```ts
15
20
  * const variants = {
16
- * visible: { opacity: 1 },
17
- * hidden: { opacity: 0 }
21
+ * visible: (i: number) => ({ x: i * 50 }),
22
+ * hidden: { opacity: 0 }
18
23
  * }
19
- * resolveVariant(variants, 'visible') // { opacity: 1 }
20
- * resolveVariant(variants, 'foo') // undefined
21
- * resolveVariant(undefined, 'visible') // undefined
24
+ * resolveVariant(variants, 'visible', 3) // { x: 150 }
25
+ * resolveVariant(variants, 'hidden') // { opacity: 0 }
26
+ * resolveVariant(undefined, 'visible') // undefined
22
27
  * ```
23
28
  */
24
- export declare const resolveVariant: (variants: Variants | undefined, key: string | undefined) => DOMKeyframesDefinition | undefined;
29
+ export declare const resolveVariant: (variants: Variants | undefined, key: string | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
25
30
  /**
26
31
  * Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
27
32
  *
28
- * When `initial` is a string, looks it up in the variants object. When `initial={false}`,
29
- * returns `false` to skip initial animation. Otherwise returns the keyframes directly.
33
+ * When `initial` is a string, looks it up in the variants object (invoking
34
+ * dynamic variants with `custom`). When `initial={false}`, returns `false` to
35
+ * skip the initial animation. Otherwise returns the keyframes directly.
30
36
  *
31
37
  * @param initial - The initial prop value (keyframes, variant key, false, or undefined).
32
38
  * @param variants - The variants object for resolving string keys.
39
+ * @param custom - Forwarded to function-form variants.
33
40
  * @returns Keyframes definition, `false` to skip animation, or undefined.
34
41
  *
35
42
  * @example
36
- * ```typescript
37
- * const variants = { hidden: { opacity: 0 } }
38
- * resolveInitial('hidden', variants) // { opacity: 0 }
39
- * resolveInitial({ x: 0 }, variants) // { x: 0 }
40
- * resolveInitial(false, variants) // false
41
- * resolveInitial(undefined, variants) // undefined
43
+ * ```ts
44
+ * const variants = { hidden: (i: number) => ({ x: -i * 100 }) }
45
+ * resolveInitial('hidden', variants, 2) // { x: -200 }
46
+ * resolveInitial({ x: 0 }, variants) // { x: 0 }
47
+ * resolveInitial(false, variants) // false
48
+ * resolveInitial(undefined, variants) // undefined
42
49
  * ```
43
50
  */
44
- export declare const resolveInitial: (initial: MotionInitial, variants: Variants | undefined) => DOMKeyframesDefinition | false | undefined;
51
+ export declare const resolveInitial: (initial: MotionInitial, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | false | undefined;
45
52
  /**
46
53
  * Resolves the animate prop to keyframes, handling variant keys.
47
54
  *
48
- * When `animate` is a string, looks it up in the variants object.
49
- * Otherwise returns the keyframes directly.
55
+ * When `animate` is a string, looks it up in the variants object (invoking
56
+ * dynamic variants with `custom`). Otherwise returns the keyframes directly.
50
57
  *
51
58
  * @param animate - The animate prop value (keyframes, variant key, or undefined).
52
59
  * @param variants - The variants object for resolving string keys.
60
+ * @param custom - Forwarded to function-form variants.
53
61
  * @returns Keyframes definition or undefined.
54
- *
55
- * @example
56
- * ```typescript
57
- * const variants = { visible: { opacity: 1 } }
58
- * resolveAnimate('visible', variants) // { opacity: 1 }
59
- * resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 }
60
- * resolveAnimate(undefined, variants) // undefined
61
- * ```
62
62
  */
63
- export declare const resolveAnimate: (animate: MotionAnimate, variants: Variants | undefined) => DOMKeyframesDefinition | undefined;
63
+ export declare const resolveAnimate: (animate: MotionAnimate, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
64
64
  /**
65
65
  * Resolves the exit prop to keyframes, handling variant keys.
66
66
  *
67
- * When `exit` is a string, looks it up in the variants object.
68
- * Otherwise returns the keyframes directly. Used by AnimatePresence for exit animations.
67
+ * When `exit` is a string, looks it up in the variants object (invoking
68
+ * dynamic variants with `custom`). Otherwise returns the keyframes directly.
69
+ * Used by AnimatePresence for exit animations.
69
70
  *
70
71
  * @param exit - The exit prop value (keyframes, variant key, or undefined).
71
72
  * @param variants - The variants object for resolving string keys.
73
+ * @param custom - Forwarded to function-form variants.
72
74
  * @returns Keyframes definition or undefined.
73
- *
74
- * @example
75
- * ```typescript
76
- * const variants = { hidden: { opacity: 0 } }
77
- * resolveExit('hidden', variants) // { opacity: 0 }
78
- * resolveExit({ y: -100 }, variants) // { y: -100 }
79
- * resolveExit(undefined, variants) // undefined
80
- * ```
81
75
  */
82
- export declare const resolveExit: (exit: MotionExit, variants: Variants | undefined) => DOMKeyframesDefinition | undefined;
76
+ export declare const resolveExit: (exit: MotionExit, variants: Variants | undefined, custom?: unknown) => DOMKeyframesDefinition | undefined;
@@ -1,104 +1,101 @@
1
1
  /**
2
2
  * Resolves a variant key to its keyframes definition.
3
3
  *
4
- * Looks up a variant name in the variants object and returns the corresponding
5
- * keyframes. Returns `undefined` if the variants object or key is not provided.
4
+ * Looks up `key` in `variants`. When the entry is a function (dynamic
5
+ * variant), it's invoked with `custom` to produce keyframes matching
6
+ * framer-motion's per-instance variant pattern.
6
7
  *
7
8
  * @param variants - The variants object containing named animation states.
8
9
  * @param key - The variant key to look up.
9
- * @returns The keyframes definition for the variant, or undefined if not found.
10
+ * @param custom - Value forwarded to function-form variants. Pass-through
11
+ * `undefined` when no `custom` is in scope; the dynamic variant itself
12
+ * decides how to handle the absent input.
13
+ * @returns The keyframes definition for the variant, or `undefined` if the
14
+ * key is missing.
10
15
  *
11
16
  * @example
12
- * ```typescript
17
+ * ```ts
13
18
  * const variants = {
14
- * visible: { opacity: 1 },
15
- * hidden: { opacity: 0 }
19
+ * visible: (i: number) => ({ x: i * 50 }),
20
+ * hidden: { opacity: 0 }
16
21
  * }
17
- * resolveVariant(variants, 'visible') // { opacity: 1 }
18
- * resolveVariant(variants, 'foo') // undefined
19
- * resolveVariant(undefined, 'visible') // undefined
22
+ * resolveVariant(variants, 'visible', 3) // { x: 150 }
23
+ * resolveVariant(variants, 'hidden') // { opacity: 0 }
24
+ * resolveVariant(undefined, 'visible') // undefined
20
25
  * ```
21
26
  */
22
- export const resolveVariant = (variants, key) => {
27
+ export const resolveVariant = (variants, key, custom) => {
23
28
  if (!variants || !key)
24
29
  return undefined;
25
- return variants[key];
30
+ const entry = variants[key];
31
+ if (typeof entry === 'function')
32
+ return entry(custom);
33
+ return entry;
26
34
  };
27
35
  /**
28
36
  * Resolves the initial prop to keyframes, handling variant keys and `initial={false}`.
29
37
  *
30
- * When `initial` is a string, looks it up in the variants object. When `initial={false}`,
31
- * returns `false` to skip initial animation. Otherwise returns the keyframes directly.
38
+ * When `initial` is a string, looks it up in the variants object (invoking
39
+ * dynamic variants with `custom`). When `initial={false}`, returns `false` to
40
+ * skip the initial animation. Otherwise returns the keyframes directly.
32
41
  *
33
42
  * @param initial - The initial prop value (keyframes, variant key, false, or undefined).
34
43
  * @param variants - The variants object for resolving string keys.
44
+ * @param custom - Forwarded to function-form variants.
35
45
  * @returns Keyframes definition, `false` to skip animation, or undefined.
36
46
  *
37
47
  * @example
38
- * ```typescript
39
- * const variants = { hidden: { opacity: 0 } }
40
- * resolveInitial('hidden', variants) // { opacity: 0 }
41
- * resolveInitial({ x: 0 }, variants) // { x: 0 }
42
- * resolveInitial(false, variants) // false
43
- * resolveInitial(undefined, variants) // undefined
48
+ * ```ts
49
+ * const variants = { hidden: (i: number) => ({ x: -i * 100 }) }
50
+ * resolveInitial('hidden', variants, 2) // { x: -200 }
51
+ * resolveInitial({ x: 0 }, variants) // { x: 0 }
52
+ * resolveInitial(false, variants) // false
53
+ * resolveInitial(undefined, variants) // undefined
44
54
  * ```
45
55
  */
46
- export const resolveInitial = (initial, variants) => {
56
+ export const resolveInitial = (initial, variants, custom) => {
47
57
  if (initial === false)
48
58
  return false;
49
59
  if (initial === undefined)
50
60
  return undefined;
51
61
  if (typeof initial === 'string')
52
- return resolveVariant(variants, initial);
62
+ return resolveVariant(variants, initial, custom);
53
63
  return initial;
54
64
  };
55
65
  /**
56
66
  * Resolves the animate prop to keyframes, handling variant keys.
57
67
  *
58
- * When `animate` is a string, looks it up in the variants object.
59
- * Otherwise returns the keyframes directly.
68
+ * When `animate` is a string, looks it up in the variants object (invoking
69
+ * dynamic variants with `custom`). Otherwise returns the keyframes directly.
60
70
  *
61
71
  * @param animate - The animate prop value (keyframes, variant key, or undefined).
62
72
  * @param variants - The variants object for resolving string keys.
73
+ * @param custom - Forwarded to function-form variants.
63
74
  * @returns Keyframes definition or undefined.
64
- *
65
- * @example
66
- * ```typescript
67
- * const variants = { visible: { opacity: 1 } }
68
- * resolveAnimate('visible', variants) // { opacity: 1 }
69
- * resolveAnimate({ scale: 1.2 }, variants) // { scale: 1.2 }
70
- * resolveAnimate(undefined, variants) // undefined
71
- * ```
72
75
  */
73
- export const resolveAnimate = (animate, variants) => {
76
+ export const resolveAnimate = (animate, variants, custom) => {
74
77
  if (animate === undefined)
75
78
  return undefined;
76
79
  if (typeof animate === 'string')
77
- return resolveVariant(variants, animate);
80
+ return resolveVariant(variants, animate, custom);
78
81
  return animate;
79
82
  };
80
83
  /**
81
84
  * Resolves the exit prop to keyframes, handling variant keys.
82
85
  *
83
- * When `exit` is a string, looks it up in the variants object.
84
- * Otherwise returns the keyframes directly. Used by AnimatePresence for exit animations.
86
+ * When `exit` is a string, looks it up in the variants object (invoking
87
+ * dynamic variants with `custom`). Otherwise returns the keyframes directly.
88
+ * Used by AnimatePresence for exit animations.
85
89
  *
86
90
  * @param exit - The exit prop value (keyframes, variant key, or undefined).
87
91
  * @param variants - The variants object for resolving string keys.
92
+ * @param custom - Forwarded to function-form variants.
88
93
  * @returns Keyframes definition or undefined.
89
- *
90
- * @example
91
- * ```typescript
92
- * const variants = { hidden: { opacity: 0 } }
93
- * resolveExit('hidden', variants) // { opacity: 0 }
94
- * resolveExit({ y: -100 }, variants) // { y: -100 }
95
- * resolveExit(undefined, variants) // undefined
96
- * ```
97
94
  */
98
- export const resolveExit = (exit, variants) => {
95
+ export const resolveExit = (exit, variants, custom) => {
99
96
  if (exit === undefined)
100
97
  return undefined;
101
98
  if (typeof exit === 'string')
102
- return resolveVariant(variants, exit);
99
+ return resolveVariant(variants, exit, custom);
103
100
  return exit;
104
101
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-motion",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values. The drop-in Framer Motion alternative for Svelte and SvelteKit.",
5
5
  "keywords": [
6
6
  "svelte",