motion 12.38.0 → 12.40.0

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.
@@ -61,9 +61,7 @@
61
61
  */
62
62
  const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v);
63
63
 
64
- function isObject(value) {
65
- return typeof value === "object" && value !== null;
66
- }
64
+ const isObject = (value) => typeof value === "object" && value !== null;
67
65
 
68
66
  /**
69
67
  * Check if the value is a zero value string like "0px" or "0%"
@@ -90,8 +88,7 @@
90
88
  * @param {...functions} transformers
91
89
  * @return {function}
92
90
  */
93
- const combineFunctions = (a, b) => (v) => b(a(v));
94
- const pipe = (...transformers) => transformers.reduce(combineFunctions);
91
+ const pipe = (...transformers) => transformers.reduce((a, b) => (v) => b(a(v)));
95
92
 
96
93
  /*
97
94
  Progress within given range
@@ -99,16 +96,11 @@
99
96
  Given a lower limit and an upper limit, we return the progress
100
97
  (expressed as a number 0-1) represented by the given value, and
101
98
  limit that progress to within 0-1.
102
-
103
- @param [number]: Lower limit
104
- @param [number]: Upper limit
105
- @param [number]: Value to find progress within given range
106
- @return [number]: Progress of value within range as expressed 0-1
107
99
  */
108
100
  /*#__NO_SIDE_EFFECTS__*/
109
101
  const progress = (from, to, value) => {
110
- const toFromDifference = to - from;
111
- return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
102
+ const range = to - from;
103
+ return range ? (value - from) / range : 1;
112
104
  };
113
105
 
114
106
  class SubscriptionManager {
@@ -161,13 +153,9 @@
161
153
 
162
154
  /*
163
155
  Convert velocity into velocity per second
164
-
165
- @param [number]: Unit per frame
166
- @param [number]: Frame duration in ms
167
156
  */
168
- function velocityPerSecond(velocity, frameDuration) {
169
- return frameDuration ? velocity * (1000 / frameDuration) : 0;
170
- }
157
+ /*#__NO_SIDE_EFFECTS__*/
158
+ const velocityPerSecond = (velocity, frameDuration) => frameDuration ? velocity * (1000 / frameDuration) : 0;
171
159
 
172
160
  const warned = new Set();
173
161
  function hasWarned(message) {
@@ -224,6 +212,7 @@
224
212
  ++i < subdivisionMaxIterations);
225
213
  return currentT;
226
214
  }
215
+ /*#__NO_SIDE_EFFECTS__*/
227
216
  function cubicBezier(mX1, mY1, mX2, mY2) {
228
217
  // If this is a linear gradient, return linear easing
229
218
  if (mX1 === mY1 && mX2 === mY2)
@@ -235,10 +224,12 @@
235
224
 
236
225
  // Accepts an easing function and returns a new one that outputs mirrored values for
237
226
  // the second half of the animation. Turns easeIn into easeInOut.
227
+ /*#__NO_SIDE_EFFECTS__*/
238
228
  const mirrorEasing = (easing) => (p) => p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2;
239
229
 
240
230
  // Accepts an easing function and returns a new one that outputs reversed values.
241
231
  // Turns easeIn into easeOut.
232
+ /*#__NO_SIDE_EFFECTS__*/
242
233
  const reverseEasing = (easing) => (p) => 1 - easing(1 - p);
243
234
 
244
235
  const backOut = /*@__PURE__*/ cubicBezier(0.33, 1.53, 0.69, 0.99);
@@ -259,6 +250,7 @@
259
250
  const easeOut = /*@__PURE__*/ cubicBezier(0, 0, 0.58, 1);
260
251
  const easeInOut = /*@__PURE__*/ cubicBezier(0.42, 0, 0.58, 1);
261
252
 
253
+ /*#__NO_SIDE_EFFECTS__*/
262
254
  function steps(numSteps, direction = "end") {
263
255
  return (progress) => {
264
256
  progress =
@@ -271,14 +263,17 @@
271
263
  };
272
264
  }
273
265
 
266
+ /*#__NO_SIDE_EFFECTS__*/
274
267
  const isEasingArray = (ease) => {
275
268
  return Array.isArray(ease) && typeof ease[0] !== "number";
276
269
  };
277
270
 
271
+ /*#__NO_SIDE_EFFECTS__*/
278
272
  function getEasingForSegment(easing, i) {
279
273
  return isEasingArray(easing) ? easing[wrap(0, easing.length, i)] : easing;
280
274
  }
281
275
 
276
+ /*#__NO_SIDE_EFFECTS__*/
282
277
  const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
283
278
 
284
279
  const easingLookup = {
@@ -2166,8 +2161,15 @@
2166
2161
  ];
2167
2162
  /**
2168
2163
  * A quick lookup for transform props.
2164
+ *
2165
+ * `pathRotation` is a transform for routing purposes (skipped from raw
2166
+ * style application, wired to the transform composite, flags transform
2167
+ * dirty) but is intentionally NOT in `transformPropOrder` — it is
2168
+ * composed onto `rotate` at the build sites, not serialized in its own
2169
+ * slot, and must stay out of the order-array consumers (parse-transform,
2170
+ * unit-conversion, keys-position).
2169
2171
  */
2170
- const transformProps = /*@__PURE__*/ (() => new Set(transformPropOrder))();
2172
+ const transformProps = /*@__PURE__*/ (() => new Set([...transformPropOrder, "pathRotation"]))();
2171
2173
 
2172
2174
  const isNumOrPxType = (v) => v === number || v === px;
2173
2175
  const transformKeys = new Set(["x", "y", "z"]);
@@ -3180,270 +3182,6 @@
3180
3182
  : maxStaggerDuration - index * staggerChildren;
3181
3183
  }
3182
3184
 
3183
- /**
3184
- * Parse Framer's special CSS variable format into a CSS token and a fallback.
3185
- *
3186
- * ```
3187
- * `var(--foo, #fff)` => [`--foo`, '#fff']
3188
- * ```
3189
- *
3190
- * @param current
3191
- */
3192
- const splitCSSVariableRegex =
3193
- // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
3194
- /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
3195
- function parseCSSVariable(current) {
3196
- const match = splitCSSVariableRegex.exec(current);
3197
- if (!match)
3198
- return [,];
3199
- const [, token1, token2, fallback] = match;
3200
- return [`--${token1 ?? token2}`, fallback];
3201
- }
3202
- const maxDepth = 4;
3203
- function getVariableValue(current, element, depth = 1) {
3204
- exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
3205
- const [token, fallback] = parseCSSVariable(current);
3206
- // No CSS variable detected
3207
- if (!token)
3208
- return;
3209
- // Attempt to read this CSS variable off the element
3210
- const resolved = window.getComputedStyle(element).getPropertyValue(token);
3211
- if (resolved) {
3212
- const trimmed = resolved.trim();
3213
- return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
3214
- }
3215
- return isCSSVariableToken(fallback)
3216
- ? getVariableValue(fallback, element, depth + 1)
3217
- : fallback;
3218
- }
3219
-
3220
- const underDampedSpring = {
3221
- type: "spring",
3222
- stiffness: 500,
3223
- damping: 25,
3224
- restSpeed: 10,
3225
- };
3226
- const criticallyDampedSpring = (target) => ({
3227
- type: "spring",
3228
- stiffness: 550,
3229
- damping: target === 0 ? 2 * Math.sqrt(550) : 30,
3230
- restSpeed: 10,
3231
- });
3232
- const keyframesTransition = {
3233
- type: "keyframes",
3234
- duration: 0.8,
3235
- };
3236
- /**
3237
- * Default easing curve is a slightly shallower version of
3238
- * the default browser easing curve.
3239
- */
3240
- const ease = {
3241
- type: "keyframes",
3242
- ease: [0.25, 0.1, 0.35, 1],
3243
- duration: 0.3,
3244
- };
3245
- const getDefaultTransition = (valueKey, { keyframes }) => {
3246
- if (keyframes.length > 2) {
3247
- return keyframesTransition;
3248
- }
3249
- else if (transformProps.has(valueKey)) {
3250
- return valueKey.startsWith("scale")
3251
- ? criticallyDampedSpring(keyframes[1])
3252
- : underDampedSpring;
3253
- }
3254
- return ease;
3255
- };
3256
-
3257
- /**
3258
- * If `transition` has `inherit: true`, shallow-merge it with
3259
- * `parentTransition` (child keys win) and strip the `inherit` key.
3260
- * Otherwise return `transition` unchanged.
3261
- */
3262
- function resolveTransition(transition, parentTransition) {
3263
- if (transition?.inherit && parentTransition) {
3264
- const { inherit: _, ...rest } = transition;
3265
- return { ...parentTransition, ...rest };
3266
- }
3267
- return transition;
3268
- }
3269
-
3270
- function getValueTransition$1(transition, key) {
3271
- const valueTransition = transition?.[key] ??
3272
- transition?.["default"] ??
3273
- transition;
3274
- if (valueTransition !== transition) {
3275
- return resolveTransition(valueTransition, transition);
3276
- }
3277
- return valueTransition;
3278
- }
3279
-
3280
- const orchestrationKeys = new Set([
3281
- "when",
3282
- "delay",
3283
- "delayChildren",
3284
- "staggerChildren",
3285
- "staggerDirection",
3286
- "repeat",
3287
- "repeatType",
3288
- "repeatDelay",
3289
- "from",
3290
- "elapsed",
3291
- ]);
3292
- /**
3293
- * Decide whether a transition is defined on a given Transition.
3294
- * This filters out orchestration options and returns true
3295
- * if any options are left.
3296
- */
3297
- function isTransitionDefined(transition) {
3298
- for (const key in transition) {
3299
- if (!orchestrationKeys.has(key))
3300
- return true;
3301
- }
3302
- return false;
3303
- }
3304
-
3305
- const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
3306
- const valueTransition = getValueTransition$1(transition, name) || {};
3307
- /**
3308
- * Most transition values are currently completely overwritten by value-specific
3309
- * transitions. In the future it'd be nicer to blend these transitions. But for now
3310
- * delay actually does inherit from the root transition if not value-specific.
3311
- */
3312
- const delay = valueTransition.delay || transition.delay || 0;
3313
- /**
3314
- * Elapsed isn't a public transition option but can be passed through from
3315
- * optimized appear effects in milliseconds.
3316
- */
3317
- let { elapsed = 0 } = transition;
3318
- elapsed = elapsed - secondsToMilliseconds(delay);
3319
- const options = {
3320
- keyframes: Array.isArray(target) ? target : [null, target],
3321
- ease: "easeOut",
3322
- velocity: value.getVelocity(),
3323
- ...valueTransition,
3324
- delay: -elapsed,
3325
- onUpdate: (v) => {
3326
- value.set(v);
3327
- valueTransition.onUpdate && valueTransition.onUpdate(v);
3328
- },
3329
- onComplete: () => {
3330
- onComplete();
3331
- valueTransition.onComplete && valueTransition.onComplete();
3332
- },
3333
- name,
3334
- motionValue: value,
3335
- element: isHandoff ? undefined : element,
3336
- };
3337
- /**
3338
- * If there's no transition defined for this value, we can generate
3339
- * unique transition settings for this value.
3340
- */
3341
- if (!isTransitionDefined(valueTransition)) {
3342
- Object.assign(options, getDefaultTransition(name, options));
3343
- }
3344
- /**
3345
- * Both WAAPI and our internal animation functions use durations
3346
- * as defined by milliseconds, while our external API defines them
3347
- * as seconds.
3348
- */
3349
- options.duration && (options.duration = secondsToMilliseconds(options.duration));
3350
- options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
3351
- /**
3352
- * Support deprecated way to set initial value. Prefer keyframe syntax.
3353
- */
3354
- if (options.from !== undefined) {
3355
- options.keyframes[0] = options.from;
3356
- }
3357
- let shouldSkip = false;
3358
- if (options.type === false ||
3359
- (options.duration === 0 && !options.repeatDelay)) {
3360
- makeAnimationInstant(options);
3361
- if (options.delay === 0) {
3362
- shouldSkip = true;
3363
- }
3364
- }
3365
- if (MotionGlobalConfig.instantAnimations ||
3366
- MotionGlobalConfig.skipAnimations ||
3367
- element?.shouldSkipAnimations) {
3368
- shouldSkip = true;
3369
- makeAnimationInstant(options);
3370
- options.delay = 0;
3371
- }
3372
- /**
3373
- * If the transition type or easing has been explicitly set by the user
3374
- * then we don't want to allow flattening the animation.
3375
- */
3376
- options.allowFlatten = !valueTransition.type && !valueTransition.ease;
3377
- /**
3378
- * If we can or must skip creating the animation, and apply only
3379
- * the final keyframe, do so. We also check once keyframes are resolved but
3380
- * this early check prevents the need to create an animation at all.
3381
- */
3382
- if (shouldSkip && !isHandoff && value.get() !== undefined) {
3383
- const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
3384
- if (finalKeyframe !== undefined) {
3385
- frame.update(() => {
3386
- options.onUpdate(finalKeyframe);
3387
- options.onComplete();
3388
- });
3389
- return;
3390
- }
3391
- }
3392
- return valueTransition.isSync
3393
- ? new JSAnimation(options)
3394
- : new AsyncMotionValueAnimation(options);
3395
- };
3396
-
3397
- function getValueState(visualElement) {
3398
- const state = [{}, {}];
3399
- visualElement?.values.forEach((value, key) => {
3400
- state[0][key] = value.get();
3401
- state[1][key] = value.getVelocity();
3402
- });
3403
- return state;
3404
- }
3405
- function resolveVariantFromProps(props, definition, custom, visualElement) {
3406
- /**
3407
- * If the variant definition is a function, resolve.
3408
- */
3409
- if (typeof definition === "function") {
3410
- const [current, velocity] = getValueState(visualElement);
3411
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3412
- }
3413
- /**
3414
- * If the variant definition is a variant label, or
3415
- * the function returned a variant label, resolve.
3416
- */
3417
- if (typeof definition === "string") {
3418
- definition = props.variants && props.variants[definition];
3419
- }
3420
- /**
3421
- * At this point we've resolved both functions and variant labels,
3422
- * but the resolved variant label might itself have been a function.
3423
- * If so, resolve. This can only have returned a valid target object.
3424
- */
3425
- if (typeof definition === "function") {
3426
- const [current, velocity] = getValueState(visualElement);
3427
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3428
- }
3429
- return definition;
3430
- }
3431
-
3432
- function resolveVariant(visualElement, definition, custom) {
3433
- const props = visualElement.getProps();
3434
- return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3435
- }
3436
-
3437
- const positionalKeys = new Set([
3438
- "width",
3439
- "height",
3440
- "top",
3441
- "left",
3442
- "right",
3443
- "bottom",
3444
- ...transformPropOrder,
3445
- ]);
3446
-
3447
3185
  /**
3448
3186
  * Maximum time between the value of two frames, beyond which we
3449
3187
  * assume the velocity has since been 0.
@@ -3652,116 +3390,562 @@
3652
3390
  }
3653
3391
  }
3654
3392
  /**
3655
- * Returns the latest state of `MotionValue`
3656
- *
3657
- * @returns - The latest state of `MotionValue`
3658
- *
3659
- * @public
3393
+ * Returns the latest state of `MotionValue`
3394
+ *
3395
+ * @returns - The latest state of `MotionValue`
3396
+ *
3397
+ * @public
3398
+ */
3399
+ get() {
3400
+ if (collectMotionValues.current) {
3401
+ collectMotionValues.current.push(this);
3402
+ }
3403
+ return this.current;
3404
+ }
3405
+ /**
3406
+ * @public
3407
+ */
3408
+ getPrevious() {
3409
+ return this.prev;
3410
+ }
3411
+ /**
3412
+ * Returns the latest velocity of `MotionValue`
3413
+ *
3414
+ * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
3415
+ *
3416
+ * @public
3417
+ */
3418
+ getVelocity() {
3419
+ const currentTime = time.now();
3420
+ if (!this.canTrackVelocity ||
3421
+ this.prevFrameValue === undefined ||
3422
+ currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
3423
+ return 0;
3424
+ }
3425
+ const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
3426
+ // Casts because of parseFloat's poor typing
3427
+ return velocityPerSecond(parseFloat(this.current) -
3428
+ parseFloat(this.prevFrameValue), delta);
3429
+ }
3430
+ /**
3431
+ * Registers a new animation to control this `MotionValue`. Only one
3432
+ * animation can drive a `MotionValue` at one time.
3433
+ *
3434
+ * ```jsx
3435
+ * value.start()
3436
+ * ```
3437
+ *
3438
+ * @param animation - A function that starts the provided animation
3439
+ */
3440
+ start(startAnimation) {
3441
+ this.stop();
3442
+ return new Promise((resolve) => {
3443
+ this.hasAnimated = true;
3444
+ this.animation = startAnimation(resolve);
3445
+ if (this.events.animationStart) {
3446
+ this.events.animationStart.notify();
3447
+ }
3448
+ }).then(() => {
3449
+ if (this.events.animationComplete) {
3450
+ this.events.animationComplete.notify();
3451
+ }
3452
+ this.clearAnimation();
3453
+ });
3454
+ }
3455
+ /**
3456
+ * Stop the currently active animation.
3457
+ *
3458
+ * @public
3459
+ */
3460
+ stop() {
3461
+ if (this.animation) {
3462
+ this.animation.stop();
3463
+ if (this.events.animationCancel) {
3464
+ this.events.animationCancel.notify();
3465
+ }
3466
+ }
3467
+ this.clearAnimation();
3468
+ }
3469
+ /**
3470
+ * Returns `true` if this value is currently animating.
3471
+ *
3472
+ * @public
3473
+ */
3474
+ isAnimating() {
3475
+ return !!this.animation;
3476
+ }
3477
+ clearAnimation() {
3478
+ delete this.animation;
3479
+ }
3480
+ /**
3481
+ * Destroy and clean up subscribers to this `MotionValue`.
3482
+ *
3483
+ * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
3484
+ * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
3485
+ * created a `MotionValue` via the `motionValue` function.
3486
+ *
3487
+ * @public
3488
+ */
3489
+ destroy() {
3490
+ this.dependents?.clear();
3491
+ this.events.destroy?.notify();
3492
+ this.clearListeners();
3493
+ this.stop();
3494
+ if (this.stopPassiveEffect) {
3495
+ this.stopPassiveEffect();
3496
+ }
3497
+ }
3498
+ }
3499
+ function motionValue(init, options) {
3500
+ return new MotionValue(init, options);
3501
+ }
3502
+
3503
+ /**
3504
+ * If `transition` has `inherit: true`, shallow-merge it with
3505
+ * `parentTransition` (child keys win) and strip the `inherit` key.
3506
+ * Otherwise return `transition` unchanged.
3507
+ */
3508
+ function resolveTransition(transition, parentTransition) {
3509
+ if (transition?.inherit && parentTransition) {
3510
+ const { inherit: _, ...rest } = transition;
3511
+ return { ...parentTransition, ...rest };
3512
+ }
3513
+ return transition;
3514
+ }
3515
+
3516
+ function getValueTransition$1(transition, key) {
3517
+ const valueTransition = transition?.[key] ??
3518
+ transition?.["default"] ??
3519
+ transition;
3520
+ if (valueTransition !== transition) {
3521
+ return resolveTransition(valueTransition, transition);
3522
+ }
3523
+ return valueTransition;
3524
+ }
3525
+
3526
+ const underDampedSpring = {
3527
+ type: "spring",
3528
+ stiffness: 500,
3529
+ damping: 25,
3530
+ restSpeed: 10,
3531
+ };
3532
+ const criticallyDampedSpring = (target) => ({
3533
+ type: "spring",
3534
+ stiffness: 550,
3535
+ damping: target === 0 ? 2 * Math.sqrt(550) : 30,
3536
+ restSpeed: 10,
3537
+ });
3538
+ const keyframesTransition = {
3539
+ type: "keyframes",
3540
+ duration: 0.8,
3541
+ };
3542
+ /**
3543
+ * Default easing curve is a slightly shallower version of
3544
+ * the default browser easing curve.
3545
+ */
3546
+ const ease = {
3547
+ type: "keyframes",
3548
+ ease: [0.25, 0.1, 0.35, 1],
3549
+ duration: 0.3,
3550
+ };
3551
+ const getDefaultTransition = (valueKey, { keyframes }) => {
3552
+ if (keyframes.length > 2) {
3553
+ return keyframesTransition;
3554
+ }
3555
+ else if (transformProps.has(valueKey)) {
3556
+ return valueKey.startsWith("scale")
3557
+ ? criticallyDampedSpring(keyframes[1])
3558
+ : underDampedSpring;
3559
+ }
3560
+ return ease;
3561
+ };
3562
+
3563
+ const orchestrationKeys = new Set([
3564
+ "when",
3565
+ "delay",
3566
+ "delayChildren",
3567
+ "staggerChildren",
3568
+ "staggerDirection",
3569
+ "repeat",
3570
+ "repeatType",
3571
+ "repeatDelay",
3572
+ "from",
3573
+ "elapsed",
3574
+ ]);
3575
+ /**
3576
+ * Decide whether a transition is defined on a given Transition.
3577
+ * This filters out orchestration options and returns true
3578
+ * if any options are left.
3579
+ */
3580
+ function isTransitionDefined(transition) {
3581
+ for (const key in transition) {
3582
+ if (!orchestrationKeys.has(key))
3583
+ return true;
3584
+ }
3585
+ return false;
3586
+ }
3587
+
3588
+ const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
3589
+ const valueTransition = getValueTransition$1(transition, name) || {};
3590
+ /**
3591
+ * Most transition values are currently completely overwritten by value-specific
3592
+ * transitions. In the future it'd be nicer to blend these transitions. But for now
3593
+ * delay actually does inherit from the root transition if not value-specific.
3594
+ */
3595
+ const delay = valueTransition.delay || transition.delay || 0;
3596
+ /**
3597
+ * Elapsed isn't a public transition option but can be passed through from
3598
+ * optimized appear effects in milliseconds.
3599
+ */
3600
+ let { elapsed = 0 } = transition;
3601
+ elapsed = elapsed - secondsToMilliseconds(delay);
3602
+ const options = {
3603
+ keyframes: Array.isArray(target) ? target : [null, target],
3604
+ ease: "easeOut",
3605
+ velocity: value.getVelocity(),
3606
+ ...valueTransition,
3607
+ delay: -elapsed,
3608
+ onUpdate: (v) => {
3609
+ value.set(v);
3610
+ valueTransition.onUpdate && valueTransition.onUpdate(v);
3611
+ },
3612
+ onComplete: () => {
3613
+ onComplete();
3614
+ valueTransition.onComplete && valueTransition.onComplete();
3615
+ },
3616
+ name,
3617
+ motionValue: value,
3618
+ element: isHandoff ? undefined : element,
3619
+ };
3620
+ /**
3621
+ * If there's no transition defined for this value, we can generate
3622
+ * unique transition settings for this value.
3623
+ */
3624
+ if (!isTransitionDefined(valueTransition)) {
3625
+ Object.assign(options, getDefaultTransition(name, options));
3626
+ }
3627
+ /**
3628
+ * Both WAAPI and our internal animation functions use durations
3629
+ * as defined by milliseconds, while our external API defines them
3630
+ * as seconds.
3660
3631
  */
3661
- get() {
3662
- if (collectMotionValues.current) {
3663
- collectMotionValues.current.push(this);
3632
+ options.duration && (options.duration = secondsToMilliseconds(options.duration));
3633
+ options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
3634
+ /**
3635
+ * Support deprecated way to set initial value. Prefer keyframe syntax.
3636
+ */
3637
+ if (options.from !== undefined) {
3638
+ options.keyframes[0] = options.from;
3639
+ }
3640
+ let shouldSkip = false;
3641
+ if (options.type === false ||
3642
+ (options.duration === 0 && !options.repeatDelay)) {
3643
+ makeAnimationInstant(options);
3644
+ if (options.delay === 0) {
3645
+ shouldSkip = true;
3664
3646
  }
3665
- return this.current;
3647
+ }
3648
+ if (MotionGlobalConfig.instantAnimations ||
3649
+ MotionGlobalConfig.skipAnimations ||
3650
+ element?.shouldSkipAnimations ||
3651
+ valueTransition.skipAnimations) {
3652
+ shouldSkip = true;
3653
+ makeAnimationInstant(options);
3654
+ options.delay = 0;
3666
3655
  }
3667
3656
  /**
3668
- * @public
3657
+ * If the transition type or easing has been explicitly set by the user
3658
+ * then we don't want to allow flattening the animation.
3669
3659
  */
3670
- getPrevious() {
3671
- return this.prev;
3672
- }
3660
+ options.allowFlatten = !valueTransition.type && !valueTransition.ease;
3673
3661
  /**
3674
- * Returns the latest velocity of `MotionValue`
3675
- *
3676
- * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
3677
- *
3678
- * @public
3662
+ * If we can or must skip creating the animation, and apply only
3663
+ * the final keyframe, do so. We also check once keyframes are resolved but
3664
+ * this early check prevents the need to create an animation at all.
3679
3665
  */
3680
- getVelocity() {
3681
- const currentTime = time.now();
3682
- if (!this.canTrackVelocity ||
3683
- this.prevFrameValue === undefined ||
3684
- currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
3685
- return 0;
3666
+ if (shouldSkip && !isHandoff && value.get() !== undefined) {
3667
+ const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
3668
+ if (finalKeyframe !== undefined) {
3669
+ frame.update(() => {
3670
+ options.onUpdate(finalKeyframe);
3671
+ options.onComplete();
3672
+ });
3673
+ return;
3686
3674
  }
3687
- const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
3688
- // Casts because of parseFloat's poor typing
3689
- return velocityPerSecond(parseFloat(this.current) -
3690
- parseFloat(this.prevFrameValue), delta);
3691
3675
  }
3692
- /**
3693
- * Registers a new animation to control this `MotionValue`. Only one
3694
- * animation can drive a `MotionValue` at one time.
3695
- *
3696
- * ```jsx
3697
- * value.start()
3698
- * ```
3699
- *
3700
- * @param animation - A function that starts the provided animation
3701
- */
3702
- start(startAnimation) {
3703
- this.stop();
3704
- return new Promise((resolve) => {
3705
- this.hasAnimated = true;
3706
- this.animation = startAnimation(resolve);
3707
- if (this.events.animationStart) {
3708
- this.events.animationStart.notify();
3676
+ return valueTransition.isSync
3677
+ ? new JSAnimation(options)
3678
+ : new AsyncMotionValueAnimation(options);
3679
+ };
3680
+
3681
+ const MIN_LAYOUT_DISTANCE = 20;
3682
+ function bezierPoint(t, origin, control, target) {
3683
+ const inv = 1 - t;
3684
+ return inv * inv * origin + 2 * inv * t * control + t * t * target;
3685
+ }
3686
+ function bezierTangentAngle(t, originX, controlX, targetX, originY, controlY, targetY) {
3687
+ const dx = 2 * (1 - t) * (controlX - originX) + 2 * t * (targetX - controlX);
3688
+ const dy = 2 * (1 - t) * (controlY - originY) + 2 * t * (targetY - controlY);
3689
+ return Math.atan2(dy, dx) * (180 / Math.PI);
3690
+ }
3691
+ function computeArcControlPoint(fromX, fromY, toX, toY, strength, peak) {
3692
+ const deltaX = toX - fromX;
3693
+ const deltaY = toY - fromY;
3694
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
3695
+ if (distance > 0) {
3696
+ const normalPerpX = -deltaY / distance;
3697
+ const normalPerpY = deltaX / distance;
3698
+ const desiredHeight = strength * distance;
3699
+ return {
3700
+ x: fromX + deltaX * peak + normalPerpX * desiredHeight,
3701
+ y: fromY + deltaY * peak + normalPerpY * desiredHeight,
3702
+ };
3703
+ }
3704
+ return { x: fromX, y: fromY };
3705
+ }
3706
+ /**
3707
+ * The pure sampling factory: `(from, to) => (t) => point`. Internal —
3708
+ * used by {@link arc} and the unit tests. Not part of the public surface.
3709
+ */
3710
+ function createArcPath({ strength = 0.5, peak = 0.5, direction, rotate = false, } = {}) {
3711
+ const rotationScale = rotate === true ? 1 : typeof rotate === "number" ? rotate : 0;
3712
+ // Auto-direction only: persists across calls to flip the bulge back
3713
+ // onto the same screen side when the dominant axis changes between
3714
+ // calls. Reuse the factory (module scope / useMemo) to keep this alive.
3715
+ let prevBulgeSign;
3716
+ const createInterpolator = (from, to) => {
3717
+ const dx = to.x - from.x;
3718
+ const dy = to.y - from.y;
3719
+ let signed;
3720
+ if (direction === "cw") {
3721
+ signed = -strength;
3722
+ }
3723
+ else if (direction === "ccw") {
3724
+ signed = strength;
3725
+ }
3726
+ else {
3727
+ const dom = Math.abs(dx) >= Math.abs(dy) ? dx : dy;
3728
+ signed = dom < 0 ? -strength : strength;
3729
+ }
3730
+ let control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
3731
+ if (direction === undefined) {
3732
+ const isVertical = Math.abs(dx) < Math.abs(dy);
3733
+ const midX = from.x + dx * peak;
3734
+ const midY = from.y + dy * peak;
3735
+ const bulgeSign = isVertical
3736
+ ? Math.sign(control.x - midX)
3737
+ : Math.sign(control.y - midY);
3738
+ if (prevBulgeSign !== undefined &&
3739
+ bulgeSign !== 0 &&
3740
+ bulgeSign !== prevBulgeSign) {
3741
+ signed = -signed;
3742
+ control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
3743
+ }
3744
+ else if (bulgeSign !== 0) {
3745
+ prevBulgeSign = bulgeSign;
3746
+ }
3747
+ }
3748
+ const tangent0 = rotationScale
3749
+ ? bezierTangentAngle(0, from.x, control.x, to.x, from.y, control.y, to.y)
3750
+ : 0;
3751
+ const tangent1 = rotationScale
3752
+ ? bezierTangentAngle(1, from.x, control.x, to.x, from.y, control.y, to.y)
3753
+ : 0;
3754
+ const tangentDelta = rotationScale
3755
+ ? wrap(-180, 180, tangent1 - tangent0)
3756
+ : 0;
3757
+ return (t) => {
3758
+ const out = {
3759
+ x: bezierPoint(t, from.x, control.x, to.x),
3760
+ y: bezierPoint(t, from.y, control.y, to.y),
3761
+ };
3762
+ if (rotationScale) {
3763
+ const raw = bezierTangentAngle(t, from.x, control.x, to.x, from.y, control.y, to.y);
3764
+ const baseline = tangent0 + tangentDelta * t;
3765
+ out.rotate = wrap(-180, 180, raw - baseline) * rotationScale;
3709
3766
  }
3710
- }).then(() => {
3711
- if (this.events.animationComplete) {
3712
- this.events.animationComplete.notify();
3767
+ return out;
3768
+ };
3769
+ };
3770
+ return createInterpolator;
3771
+ }
3772
+ /**
3773
+ * Creates a curved path for `transition.path`:
3774
+ *
3775
+ * ```ts
3776
+ * <motion.div animate={{ x: 200, y: 100 }} transition={{ path: arc() }} />
3777
+ * ```
3778
+ *
3779
+ * Reuse the returned value (module scope / useMemo / useRef) so its
3780
+ * continuity closure survives re-renders — a fresh `arc()` has no memory.
3781
+ */
3782
+ function arc(options = {}) {
3783
+ const sample = createArcPath(options);
3784
+ const path = {
3785
+ interpolateProjection(delta) {
3786
+ // `from` is the current translate offset (carries any in-flight
3787
+ // displacement when interrupted); `to` is the new layout origin.
3788
+ // The distance floor avoids visible wobble on tiny shifts.
3789
+ const tx = delta.x.translate;
3790
+ const ty = delta.y.translate;
3791
+ if (Math.sqrt(tx * tx + ty * ty) < MIN_LAYOUT_DISTANCE) {
3792
+ return undefined;
3713
3793
  }
3714
- this.clearAnimation();
3715
- });
3794
+ return sample({ x: tx, y: ty }, { x: 0, y: 0 });
3795
+ },
3796
+ animateVisualElement(visualElement, target, transition, delay, animations) {
3797
+ if (!("x" in target || "y" in target))
3798
+ return;
3799
+ const xValue = visualElement.getValue("x", visualElement.latestValues["x"] ?? 0);
3800
+ const yValue = visualElement.getValue("y", visualElement.latestValues["y"] ?? 0);
3801
+ const xRaw = target.x;
3802
+ const yRaw = target.y;
3803
+ const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
3804
+ ? xRaw[0]
3805
+ : xValue?.get()) ?? 0;
3806
+ const yFrom = (Array.isArray(yRaw) && yRaw[0] != null
3807
+ ? yRaw[0]
3808
+ : yValue?.get()) ?? 0;
3809
+ const xTo = (Array.isArray(xRaw)
3810
+ ? xRaw[xRaw.length - 1]
3811
+ : xRaw ?? xFrom);
3812
+ const yTo = (Array.isArray(yRaw)
3813
+ ? yRaw[yRaw.length - 1]
3814
+ : yRaw ?? yFrom);
3815
+ // Interruption needs no flag: x/y already hold the displaced
3816
+ // mid-arc position, so xFrom/yFrom carry the continuity geometry.
3817
+ const interpolate = sample({ x: xFrom, y: yFrom }, { x: xTo, y: yTo });
3818
+ // Drive a dedicated `pathRotation` value (composed onto `rotate`
3819
+ // at the build sites) rather than `rotate` itself, so a
3820
+ // concurrent rotate animation composes and nothing accumulates
3821
+ // on interrupt.
3822
+ const pathRotationValue = interpolate(0).rotate !== undefined
3823
+ ? visualElement.getValue("pathRotation", 0)
3824
+ : undefined;
3825
+ const pathTransition = {
3826
+ delay,
3827
+ ...getValueTransition$1(transition || {}, "x"),
3828
+ };
3829
+ delete pathTransition.path;
3830
+ const progress = motionValue(0);
3831
+ progress.start(animateMotionValue("", progress, [0, 1000], {
3832
+ ...pathTransition,
3833
+ isSync: true,
3834
+ velocity: 0,
3835
+ onUpdate: (latest) => {
3836
+ const point = interpolate(latest / 1000);
3837
+ xValue?.set(point.x);
3838
+ yValue?.set(point.y);
3839
+ if (pathRotationValue && point.rotate !== undefined) {
3840
+ pathRotationValue.set(point.rotate);
3841
+ }
3842
+ },
3843
+ onComplete: () => {
3844
+ xValue?.set(xTo);
3845
+ yValue?.set(yTo);
3846
+ pathRotationValue?.set(0);
3847
+ },
3848
+ // Interrupt/cancel must clear our additive contribution
3849
+ // so it can't linger on top of the user's `rotate`.
3850
+ onStop: () => pathRotationValue?.set(0),
3851
+ onCancel: () => pathRotationValue?.set(0),
3852
+ }));
3853
+ if (progress.animation)
3854
+ animations.push(progress.animation);
3855
+ delete target.x;
3856
+ delete target.y;
3857
+ },
3858
+ };
3859
+ return path;
3860
+ }
3861
+
3862
+ /**
3863
+ * Parse Framer's special CSS variable format into a CSS token and a fallback.
3864
+ *
3865
+ * ```
3866
+ * `var(--foo, #fff)` => [`--foo`, '#fff']
3867
+ * ```
3868
+ *
3869
+ * @param current
3870
+ */
3871
+ const splitCSSVariableRegex =
3872
+ // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
3873
+ /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
3874
+ function parseCSSVariable(current) {
3875
+ const match = splitCSSVariableRegex.exec(current);
3876
+ if (!match)
3877
+ return [,];
3878
+ const [, token1, token2, fallback] = match;
3879
+ return [`--${token1 ?? token2}`, fallback];
3880
+ }
3881
+ const maxDepth = 4;
3882
+ function getVariableValue(current, element, depth = 1) {
3883
+ exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
3884
+ const [token, fallback] = parseCSSVariable(current);
3885
+ // No CSS variable detected
3886
+ if (!token)
3887
+ return;
3888
+ // Attempt to read this CSS variable off the element
3889
+ const resolved = window.getComputedStyle(element).getPropertyValue(token);
3890
+ if (resolved) {
3891
+ const trimmed = resolved.trim();
3892
+ return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
3716
3893
  }
3894
+ return isCSSVariableToken(fallback)
3895
+ ? getVariableValue(fallback, element, depth + 1)
3896
+ : fallback;
3897
+ }
3898
+
3899
+ function getValueState(visualElement) {
3900
+ const state = [{}, {}];
3901
+ visualElement?.values.forEach((value, key) => {
3902
+ state[0][key] = value.get();
3903
+ state[1][key] = value.getVelocity();
3904
+ });
3905
+ return state;
3906
+ }
3907
+ function resolveVariantFromProps(props, definition, custom, visualElement) {
3717
3908
  /**
3718
- * Stop the currently active animation.
3719
- *
3720
- * @public
3909
+ * If the variant definition is a function, resolve.
3721
3910
  */
3722
- stop() {
3723
- if (this.animation) {
3724
- this.animation.stop();
3725
- if (this.events.animationCancel) {
3726
- this.events.animationCancel.notify();
3727
- }
3728
- }
3729
- this.clearAnimation();
3911
+ if (typeof definition === "function") {
3912
+ const [current, velocity] = getValueState(visualElement);
3913
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3730
3914
  }
3731
3915
  /**
3732
- * Returns `true` if this value is currently animating.
3733
- *
3734
- * @public
3916
+ * If the variant definition is a variant label, or
3917
+ * the function returned a variant label, resolve.
3735
3918
  */
3736
- isAnimating() {
3737
- return !!this.animation;
3738
- }
3739
- clearAnimation() {
3740
- delete this.animation;
3919
+ if (typeof definition === "string") {
3920
+ definition = props.variants && props.variants[definition];
3741
3921
  }
3742
3922
  /**
3743
- * Destroy and clean up subscribers to this `MotionValue`.
3744
- *
3745
- * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
3746
- * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
3747
- * created a `MotionValue` via the `motionValue` function.
3748
- *
3749
- * @public
3923
+ * At this point we've resolved both functions and variant labels,
3924
+ * but the resolved variant label might itself have been a function.
3925
+ * If so, resolve. This can only have returned a valid target object.
3750
3926
  */
3751
- destroy() {
3752
- this.dependents?.clear();
3753
- this.events.destroy?.notify();
3754
- this.clearListeners();
3755
- this.stop();
3756
- if (this.stopPassiveEffect) {
3757
- this.stopPassiveEffect();
3758
- }
3927
+ if (typeof definition === "function") {
3928
+ const [current, velocity] = getValueState(visualElement);
3929
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3759
3930
  }
3931
+ return definition;
3760
3932
  }
3761
- function motionValue(init, options) {
3762
- return new MotionValue(init, options);
3933
+
3934
+ function resolveVariant(visualElement, definition, custom) {
3935
+ const props = visualElement.getProps();
3936
+ return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3763
3937
  }
3764
3938
 
3939
+ const positionalKeys = new Set([
3940
+ "width",
3941
+ "height",
3942
+ "top",
3943
+ "left",
3944
+ "right",
3945
+ "bottom",
3946
+ ...transformPropOrder,
3947
+ ]);
3948
+
3765
3949
  const isKeyframesTarget = (v) => {
3766
3950
  return Array.isArray(v);
3767
3951
  };
@@ -3843,12 +4027,18 @@
3843
4027
  ? resolveTransition(transition, defaultTransition)
3844
4028
  : defaultTransition;
3845
4029
  const reduceMotion = transition?.reduceMotion;
4030
+ const skipAnimations = transition?.skipAnimations;
3846
4031
  if (transitionOverride)
3847
4032
  transition = transitionOverride;
3848
4033
  const animations = [];
3849
4034
  const animationTypeState = type &&
3850
4035
  visualElement.animationState &&
3851
4036
  visualElement.animationState.getState()[type];
4037
+ const path = transition?.path;
4038
+ if (path) {
4039
+ // path mutates `target` to claim x/y; loop below skips them.
4040
+ path.animateVisualElement(visualElement, target, transition, delay, animations);
4041
+ }
3852
4042
  for (const key in target) {
3853
4043
  const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
3854
4044
  const valueTarget = target[key];
@@ -3861,6 +4051,8 @@
3861
4051
  delay,
3862
4052
  ...getValueTransition$1(transition || {}, key),
3863
4053
  };
4054
+ if (skipAnimations)
4055
+ valueTransition.skipAnimations = true;
3864
4056
  /**
3865
4057
  * If the value is already at the defined target, skip the animation.
3866
4058
  * We still re-assert the value via frame.update to take precedence
@@ -4065,6 +4257,12 @@
4065
4257
 
4066
4258
  const transformValueTypes = {
4067
4259
  rotate: degrees,
4260
+ /**
4261
+ * Internal channel for `transition.path` orientToPath. Composed onto
4262
+ * `rotate` at the transform-build sites so the user's `rotate` is
4263
+ * never read or overwritten. Not part of `transformPropOrder`.
4264
+ */
4265
+ pathRotation: degrees,
4068
4266
  rotateX: degrees,
4069
4267
  rotateY: degrees,
4070
4268
  rotateZ: degrees,
@@ -4616,6 +4814,15 @@
4616
4814
  transform += `${transformName}(${value}) `;
4617
4815
  }
4618
4816
  }
4817
+ // See build-transform.ts: additive `rotate()` so user `rotate` isn't
4818
+ // clobbered. Not a `transformPropOrder` slot.
4819
+ const pathRotation = state.latest.pathRotation;
4820
+ if (pathRotation) {
4821
+ transformIsDefault = false;
4822
+ transform += `rotate(${typeof pathRotation === "number"
4823
+ ? `${pathRotation}deg`
4824
+ : pathRotation}) `;
4825
+ }
4619
4826
  return transformIsDefault ? "none" : transform.trim();
4620
4827
  }
4621
4828
 
@@ -6225,8 +6432,8 @@
6225
6432
  removeOnChange();
6226
6433
  if (removeSyncCheck)
6227
6434
  removeSyncCheck();
6228
- if (value.owner)
6229
- value.stop();
6435
+ // Defer to MotionValue.on("change") auto-stop so React 19 remounts
6436
+ // can resubscribe before the animation is cancelled (#3315).
6230
6437
  });
6231
6438
  }
6232
6439
  sortNodePosition(other) {
@@ -6768,6 +6975,14 @@
6768
6975
  }
6769
6976
  }
6770
6977
  }
6978
+ // `pathRotation` composes onto `rotate` as a separate additive term so
6979
+ // the user's `rotate` is never clobbered. Deliberately not a slot in
6980
+ // `transformPropOrder`.
6981
+ const pathRotation = latestValues.pathRotation;
6982
+ if (pathRotation) {
6983
+ transformIsDefault = false;
6984
+ transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
6985
+ }
6771
6986
  transformString = transformString.trim();
6772
6987
  // If we have a custom `transform` template, pass our transform values and
6773
6988
  // generated transformString to that before returning
@@ -7470,7 +7685,8 @@
7470
7685
  */
7471
7686
  let valueHasChanged = false;
7472
7687
  if (isKeyframesTarget(next) && isKeyframesTarget(prev)) {
7473
- valueHasChanged = !shallowCompare(next, prev);
7688
+ valueHasChanged =
7689
+ !shallowCompare(next, prev) || variantDidChange;
7474
7690
  }
7475
7691
  else {
7476
7692
  valueHasChanged = next !== prev;
@@ -7829,11 +8045,14 @@
7829
8045
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
7830
8046
  }
7831
8047
  if (latestTransform) {
7832
- const { transformPerspective, rotate, rotateX, rotateY, skewX, skewY } = latestTransform;
8048
+ const { transformPerspective, rotate, pathRotation, rotateX, rotateY, skewX, skewY, } = latestTransform;
7833
8049
  if (transformPerspective)
7834
8050
  transform = `perspective(${transformPerspective}px) ${transform}`;
7835
8051
  if (rotate)
7836
8052
  transform += `rotate(${rotate}deg) `;
8053
+ // Additive `rotate()` so user `rotate` isn't clobbered.
8054
+ if (pathRotation)
8055
+ transform += `rotate(${pathRotation}deg) `;
7837
8056
  if (rotateX)
7838
8057
  transform += `rotateX(${rotateX}deg) `;
7839
8058
  if (rotateY)
@@ -8388,7 +8607,8 @@
8388
8607
  * Set animation origin after starting animation to avoid layout jump
8389
8608
  * caused by stopping previous layout animation
8390
8609
  */
8391
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
8610
+ this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions
8611
+ .path);
8392
8612
  }
8393
8613
  else {
8394
8614
  /**
@@ -9104,7 +9324,7 @@
9104
9324
  this.projectionDelta = createDelta();
9105
9325
  this.projectionDeltaWithTransform = createDelta();
9106
9326
  }
9107
- setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
9327
+ setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false, pathFn) {
9108
9328
  const snapshot = this.snapshot;
9109
9329
  const snapshotLatestValues = snapshot ? snapshot.latestValues : {};
9110
9330
  const mixedValues = { ...this.latestValues };
@@ -9126,10 +9346,26 @@
9126
9346
  !this.path.some(hasOpacityCrossfade));
9127
9347
  this.animationProgress = 0;
9128
9348
  let prevRelativeTarget;
9349
+ // The path decides whether the layout shift is worth curving
9350
+ // (distance floor) and resolves the interpolator from the delta.
9351
+ const interpolate = pathFn?.interpolateProjection(delta);
9129
9352
  this.mixTargetDelta = (latest) => {
9130
9353
  const progress = latest / 1000;
9131
- mixAxisDelta(targetDelta.x, delta.x, progress);
9132
- mixAxisDelta(targetDelta.y, delta.y, progress);
9354
+ const point = interpolate?.(progress);
9355
+ if (point) {
9356
+ targetDelta.x.translate = point.x;
9357
+ targetDelta.x.scale = mixNumber$1(delta.x.scale, 1, progress);
9358
+ targetDelta.x.origin = delta.x.origin;
9359
+ targetDelta.x.originPoint = delta.x.originPoint;
9360
+ targetDelta.y.translate = point.y;
9361
+ targetDelta.y.scale = mixNumber$1(delta.y.scale, 1, progress);
9362
+ targetDelta.y.origin = delta.y.origin;
9363
+ targetDelta.y.originPoint = delta.y.originPoint;
9364
+ }
9365
+ else {
9366
+ mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
9367
+ mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
9368
+ }
9133
9369
  this.setTargetDelta(targetDelta);
9134
9370
  if (this.relativeTarget &&
9135
9371
  this.relativeTargetOrigin &&
@@ -9154,6 +9390,13 @@
9154
9390
  this.animationValues = mixedValues;
9155
9391
  mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
9156
9392
  }
9393
+ if (point && point.rotate !== undefined) {
9394
+ // Dedicated `pathRotation` channel, not `rotate`, so an
9395
+ // animating `rotate` is composed with, never clobbered.
9396
+ if (!this.animationValues)
9397
+ this.animationValues = mixedValues;
9398
+ this.animationValues.pathRotation = point.rotate;
9399
+ }
9157
9400
  this.root.scheduleUpdateProjection();
9158
9401
  this.scheduleRender();
9159
9402
  this.animationProgress = progress;
@@ -9674,7 +9917,7 @@
9674
9917
  function removeLeadSnapshots(stack) {
9675
9918
  stack.removeLeadSnapshot();
9676
9919
  }
9677
- function mixAxisDelta(output, delta, p) {
9920
+ function mixAxisDeltaLinear(output, delta, p) {
9678
9921
  output.translate = mixNumber$1(delta.translate, 0, p);
9679
9922
  output.scale = mixNumber$1(delta.scale, 1, p);
9680
9923
  output.origin = delta.origin;
@@ -10075,8 +10318,8 @@
10075
10318
  }
10076
10319
  }
10077
10320
 
10078
- function calculateRepeatDuration(duration, repeat, _repeatDelay) {
10079
- return duration * (repeat + 1);
10321
+ function calculateRepeatDuration(duration, repeat, repeatDelay) {
10322
+ return duration * (repeat + 1) + repeatDelay * repeat;
10080
10323
  }
10081
10324
 
10082
10325
  /**
@@ -10132,10 +10375,14 @@
10132
10375
  * if we have original times of [0, 0.5, 1] then our repeated times will
10133
10376
  * be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
10134
10377
  * down to a 0-1 scale.
10378
+ *
10379
+ * `repeatDelayUnits` is the repeatDelay expressed in units of a single
10380
+ * iteration's duration, so the total span equals `(repeat + 1) + repeat * repeatDelayUnits`.
10135
10381
  */
10136
- function normalizeTimes(times, repeat) {
10382
+ function normalizeTimes(times, repeat, repeatDelayUnits = 0) {
10383
+ const totalUnits = repeat + 1 + repeat * repeatDelayUnits;
10137
10384
  for (let i = 0; i < times.length; i++) {
10138
- times[i] = times[i] / (repeat + 1);
10385
+ times[i] = times[i] / totalUnits;
10139
10386
  }
10140
10387
  }
10141
10388
 
@@ -10256,25 +10503,73 @@
10256
10503
  valueKeyframesAsList.length === 1 &&
10257
10504
  valueKeyframesAsList.unshift(null);
10258
10505
  /**
10259
- * Handle repeat options
10506
+ * Segments can't express `repeat: Infinity` or very large
10507
+ * counts — they'd leave dead time after the segment or
10508
+ * explode the keyframe array. Ignore with a warning.
10260
10509
  */
10261
10510
  if (repeat) {
10262
- exports.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20", "repeat-count-high");
10263
- duration = calculateRepeatDuration(duration, repeat);
10511
+ exports.warning(repeat < MAX_REPEAT, `Sequence segments can't repeat ${repeat} times ignoring repeat option. Use a value below ${MAX_REPEAT} or apply repeat at the sequence level instead.`);
10512
+ }
10513
+ if (repeat && repeat < MAX_REPEAT) {
10514
+ /**
10515
+ * Express repeatDelay in units of a single iteration's duration
10516
+ * so it can be added to the per-iteration time offsets below
10517
+ * before they're normalized to 0-1.
10518
+ */
10519
+ const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
10520
+ duration = calculateRepeatDuration(duration, repeat, repeatDelay);
10264
10521
  const originalKeyframes = [...valueKeyframesAsList];
10265
10522
  const originalTimes = [...times];
10266
10523
  ease = Array.isArray(ease) ? [...ease] : [ease];
10267
10524
  const originalEase = [...ease];
10525
+ /**
10526
+ * For reverse/mirror, alternate iterations play the segment
10527
+ * backwards. mirror matches JSAnimation's mirroredGenerator:
10528
+ * reversed keyframes, easings unchanged. reverse matches
10529
+ * JSAnimation's iterationProgress = 1 - p: reversed
10530
+ * keyframes, easing array reversed AND each function easing
10531
+ * mapped through reverseEasing (string easings unchanged —
10532
+ * they're resolved later by the keyframes engine).
10533
+ */
10534
+ const isFlipping = repeatType === "reverse" || repeatType === "mirror";
10535
+ let flippedKeyframes = originalKeyframes;
10536
+ let flippedEases = originalEase;
10537
+ if (isFlipping) {
10538
+ flippedKeyframes = [...originalKeyframes].reverse();
10539
+ if (repeatType === "reverse") {
10540
+ flippedEases = [...originalEase]
10541
+ .reverse()
10542
+ .map((e) => typeof e === "function"
10543
+ ? reverseEasing(e)
10544
+ : e);
10545
+ }
10546
+ }
10268
10547
  for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
10269
- valueKeyframesAsList.push(...originalKeyframes);
10270
- for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
10271
- times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
10548
+ const isFlipped = isFlipping && repeatIndex % 2 === 0;
10549
+ const iterKeyframes = isFlipped
10550
+ ? flippedKeyframes
10551
+ : originalKeyframes;
10552
+ const iterEase = isFlipped ? flippedEases : originalEase;
10553
+ const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
10554
+ /**
10555
+ * If repeatDelay is set, hold the previous iteration's
10556
+ * final value through the delay by inserting a keyframe
10557
+ * at the moment the next iteration begins.
10558
+ */
10559
+ if (repeatDelayUnits > 0) {
10560
+ valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
10561
+ times.push(iterStartOffset);
10562
+ ease.push("linear");
10563
+ }
10564
+ valueKeyframesAsList.push(...iterKeyframes);
10565
+ for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
10566
+ times.push(originalTimes[keyframeIndex] + iterStartOffset);
10272
10567
  ease.push(keyframeIndex === 0
10273
10568
  ? "linear"
10274
- : getEasingForSegment(originalEase, keyframeIndex - 1));
10569
+ : getEasingForSegment(iterEase, keyframeIndex - 1));
10275
10570
  }
10276
10571
  }
10277
- normalizeTimes(times, repeat);
10572
+ normalizeTimes(times, repeat, repeatDelayUnits);
10278
10573
  }
10279
10574
  const targetTime = startTime + duration;
10280
10575
  /**
@@ -10522,21 +10817,24 @@
10522
10817
  * to a specific element.
10523
10818
  */
10524
10819
  function createScopedAnimate(options = {}) {
10525
- const { scope, reduceMotion } = options;
10820
+ const { scope, reduceMotion, skipAnimations } = options;
10526
10821
  /**
10527
10822
  * Implementation
10528
10823
  */
10529
10824
  function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
10530
10825
  let animations = [];
10531
10826
  let animationOnComplete;
10827
+ const inherited = {};
10828
+ if (reduceMotion !== undefined)
10829
+ inherited.reduceMotion = reduceMotion;
10830
+ if (skipAnimations !== undefined)
10831
+ inherited.skipAnimations = skipAnimations;
10532
10832
  if (isSequence(subjectOrSequence)) {
10533
10833
  const { onComplete, ...sequenceOptions } = optionsOrKeyframes || {};
10534
10834
  if (typeof onComplete === "function") {
10535
10835
  animationOnComplete = onComplete;
10536
10836
  }
10537
- animations = animateSequence(subjectOrSequence, reduceMotion !== undefined
10538
- ? { reduceMotion, ...sequenceOptions }
10539
- : sequenceOptions, scope);
10837
+ animations = animateSequence(subjectOrSequence, { ...inherited, ...sequenceOptions }, scope);
10540
10838
  }
10541
10839
  else {
10542
10840
  // Extract top-level onComplete so it doesn't get applied per-value
@@ -10544,9 +10842,7 @@
10544
10842
  if (typeof onComplete === "function") {
10545
10843
  animationOnComplete = onComplete;
10546
10844
  }
10547
- animations = animateSubject(subjectOrSequence, optionsOrKeyframes, (reduceMotion !== undefined
10548
- ? { reduceMotion, ...rest }
10549
- : rest), scope);
10845
+ animations = animateSubject(subjectOrSequence, optionsOrKeyframes, { ...inherited, ...rest }, scope);
10550
10846
  }
10551
10847
  const animation = new GroupAnimationWithThen(animations);
10552
10848
  if (animationOnComplete) {
@@ -10948,10 +11244,16 @@
10948
11244
  info.y.containerLength = container.clientHeight;
10949
11245
  /**
10950
11246
  * In development mode ensure scroll containers aren't position: static as this makes
10951
- * it difficult to measure their relative positions.
11247
+ * it difficult to measure their relative positions. The document scrolling element
11248
+ * is exempt: offsetParent measurements naturally resolve relative to the document.
10952
11249
  */
10953
11250
  {
10954
- if (container && target && target !== container) {
11251
+ if (container &&
11252
+ target &&
11253
+ target !== container &&
11254
+ container !== document.documentElement &&
11255
+ container !== document.scrollingElement &&
11256
+ container !== document.body) {
10955
11257
  warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
10956
11258
  }
10957
11259
  }
@@ -11233,6 +11535,14 @@
11233
11535
  });
11234
11536
  }
11235
11537
 
11538
+ /**
11539
+ * Currently, we only support element tracking with `scrollInfo`, though in
11540
+ * the future we can also offer ViewTimeline support.
11541
+ */
11542
+ function isElementTracking(options) {
11543
+ return options && (options.target || options.offset);
11544
+ }
11545
+
11236
11546
  /**
11237
11547
  * If the onScroll function has two arguments, it's expecting
11238
11548
  * more specific information about the scroll from scrollInfo.
@@ -11241,7 +11551,7 @@
11241
11551
  return onScroll.length === 2;
11242
11552
  }
11243
11553
  function attachToFunction(onScroll, options) {
11244
- if (isOnScrollWithInfo(onScroll)) {
11554
+ if (isOnScrollWithInfo(onScroll) || isElementTracking(options)) {
11245
11555
  return scrollInfo((info) => {
11246
11556
  onScroll(info[options.axis].progress, info);
11247
11557
  }, options);
@@ -11359,6 +11669,7 @@
11359
11669
  exports.applyPointDelta = applyPointDelta;
11360
11670
  exports.applyPxDefaults = applyPxDefaults;
11361
11671
  exports.applyTreeDeltas = applyTreeDeltas;
11672
+ exports.arc = arc;
11362
11673
  exports.aspectRatio = aspectRatio;
11363
11674
  exports.attachFollow = attachFollow;
11364
11675
  exports.attachSpring = attachSpring;