motion 12.39.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.
@@ -2161,8 +2161,15 @@
2161
2161
  ];
2162
2162
  /**
2163
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).
2164
2171
  */
2165
- const transformProps = /*@__PURE__*/ (() => new Set(transformPropOrder))();
2172
+ const transformProps = /*@__PURE__*/ (() => new Set([...transformPropOrder, "pathRotation"]))();
2166
2173
 
2167
2174
  const isNumOrPxType = (v) => v === number || v === px;
2168
2175
  const transformKeys = new Set(["x", "y", "z"]);
@@ -3175,271 +3182,6 @@
3175
3182
  : maxStaggerDuration - index * staggerChildren;
3176
3183
  }
3177
3184
 
3178
- /**
3179
- * Parse Framer's special CSS variable format into a CSS token and a fallback.
3180
- *
3181
- * ```
3182
- * `var(--foo, #fff)` => [`--foo`, '#fff']
3183
- * ```
3184
- *
3185
- * @param current
3186
- */
3187
- const splitCSSVariableRegex =
3188
- // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
3189
- /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
3190
- function parseCSSVariable(current) {
3191
- const match = splitCSSVariableRegex.exec(current);
3192
- if (!match)
3193
- return [,];
3194
- const [, token1, token2, fallback] = match;
3195
- return [`--${token1 ?? token2}`, fallback];
3196
- }
3197
- const maxDepth = 4;
3198
- function getVariableValue(current, element, depth = 1) {
3199
- exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
3200
- const [token, fallback] = parseCSSVariable(current);
3201
- // No CSS variable detected
3202
- if (!token)
3203
- return;
3204
- // Attempt to read this CSS variable off the element
3205
- const resolved = window.getComputedStyle(element).getPropertyValue(token);
3206
- if (resolved) {
3207
- const trimmed = resolved.trim();
3208
- return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
3209
- }
3210
- return isCSSVariableToken(fallback)
3211
- ? getVariableValue(fallback, element, depth + 1)
3212
- : fallback;
3213
- }
3214
-
3215
- const underDampedSpring = {
3216
- type: "spring",
3217
- stiffness: 500,
3218
- damping: 25,
3219
- restSpeed: 10,
3220
- };
3221
- const criticallyDampedSpring = (target) => ({
3222
- type: "spring",
3223
- stiffness: 550,
3224
- damping: target === 0 ? 2 * Math.sqrt(550) : 30,
3225
- restSpeed: 10,
3226
- });
3227
- const keyframesTransition = {
3228
- type: "keyframes",
3229
- duration: 0.8,
3230
- };
3231
- /**
3232
- * Default easing curve is a slightly shallower version of
3233
- * the default browser easing curve.
3234
- */
3235
- const ease = {
3236
- type: "keyframes",
3237
- ease: [0.25, 0.1, 0.35, 1],
3238
- duration: 0.3,
3239
- };
3240
- const getDefaultTransition = (valueKey, { keyframes }) => {
3241
- if (keyframes.length > 2) {
3242
- return keyframesTransition;
3243
- }
3244
- else if (transformProps.has(valueKey)) {
3245
- return valueKey.startsWith("scale")
3246
- ? criticallyDampedSpring(keyframes[1])
3247
- : underDampedSpring;
3248
- }
3249
- return ease;
3250
- };
3251
-
3252
- /**
3253
- * If `transition` has `inherit: true`, shallow-merge it with
3254
- * `parentTransition` (child keys win) and strip the `inherit` key.
3255
- * Otherwise return `transition` unchanged.
3256
- */
3257
- function resolveTransition(transition, parentTransition) {
3258
- if (transition?.inherit && parentTransition) {
3259
- const { inherit: _, ...rest } = transition;
3260
- return { ...parentTransition, ...rest };
3261
- }
3262
- return transition;
3263
- }
3264
-
3265
- function getValueTransition$1(transition, key) {
3266
- const valueTransition = transition?.[key] ??
3267
- transition?.["default"] ??
3268
- transition;
3269
- if (valueTransition !== transition) {
3270
- return resolveTransition(valueTransition, transition);
3271
- }
3272
- return valueTransition;
3273
- }
3274
-
3275
- const orchestrationKeys = new Set([
3276
- "when",
3277
- "delay",
3278
- "delayChildren",
3279
- "staggerChildren",
3280
- "staggerDirection",
3281
- "repeat",
3282
- "repeatType",
3283
- "repeatDelay",
3284
- "from",
3285
- "elapsed",
3286
- ]);
3287
- /**
3288
- * Decide whether a transition is defined on a given Transition.
3289
- * This filters out orchestration options and returns true
3290
- * if any options are left.
3291
- */
3292
- function isTransitionDefined(transition) {
3293
- for (const key in transition) {
3294
- if (!orchestrationKeys.has(key))
3295
- return true;
3296
- }
3297
- return false;
3298
- }
3299
-
3300
- const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
3301
- const valueTransition = getValueTransition$1(transition, name) || {};
3302
- /**
3303
- * Most transition values are currently completely overwritten by value-specific
3304
- * transitions. In the future it'd be nicer to blend these transitions. But for now
3305
- * delay actually does inherit from the root transition if not value-specific.
3306
- */
3307
- const delay = valueTransition.delay || transition.delay || 0;
3308
- /**
3309
- * Elapsed isn't a public transition option but can be passed through from
3310
- * optimized appear effects in milliseconds.
3311
- */
3312
- let { elapsed = 0 } = transition;
3313
- elapsed = elapsed - secondsToMilliseconds(delay);
3314
- const options = {
3315
- keyframes: Array.isArray(target) ? target : [null, target],
3316
- ease: "easeOut",
3317
- velocity: value.getVelocity(),
3318
- ...valueTransition,
3319
- delay: -elapsed,
3320
- onUpdate: (v) => {
3321
- value.set(v);
3322
- valueTransition.onUpdate && valueTransition.onUpdate(v);
3323
- },
3324
- onComplete: () => {
3325
- onComplete();
3326
- valueTransition.onComplete && valueTransition.onComplete();
3327
- },
3328
- name,
3329
- motionValue: value,
3330
- element: isHandoff ? undefined : element,
3331
- };
3332
- /**
3333
- * If there's no transition defined for this value, we can generate
3334
- * unique transition settings for this value.
3335
- */
3336
- if (!isTransitionDefined(valueTransition)) {
3337
- Object.assign(options, getDefaultTransition(name, options));
3338
- }
3339
- /**
3340
- * Both WAAPI and our internal animation functions use durations
3341
- * as defined by milliseconds, while our external API defines them
3342
- * as seconds.
3343
- */
3344
- options.duration && (options.duration = secondsToMilliseconds(options.duration));
3345
- options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
3346
- /**
3347
- * Support deprecated way to set initial value. Prefer keyframe syntax.
3348
- */
3349
- if (options.from !== undefined) {
3350
- options.keyframes[0] = options.from;
3351
- }
3352
- let shouldSkip = false;
3353
- if (options.type === false ||
3354
- (options.duration === 0 && !options.repeatDelay)) {
3355
- makeAnimationInstant(options);
3356
- if (options.delay === 0) {
3357
- shouldSkip = true;
3358
- }
3359
- }
3360
- if (MotionGlobalConfig.instantAnimations ||
3361
- MotionGlobalConfig.skipAnimations ||
3362
- element?.shouldSkipAnimations ||
3363
- valueTransition.skipAnimations) {
3364
- shouldSkip = true;
3365
- makeAnimationInstant(options);
3366
- options.delay = 0;
3367
- }
3368
- /**
3369
- * If the transition type or easing has been explicitly set by the user
3370
- * then we don't want to allow flattening the animation.
3371
- */
3372
- options.allowFlatten = !valueTransition.type && !valueTransition.ease;
3373
- /**
3374
- * If we can or must skip creating the animation, and apply only
3375
- * the final keyframe, do so. We also check once keyframes are resolved but
3376
- * this early check prevents the need to create an animation at all.
3377
- */
3378
- if (shouldSkip && !isHandoff && value.get() !== undefined) {
3379
- const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
3380
- if (finalKeyframe !== undefined) {
3381
- frame.update(() => {
3382
- options.onUpdate(finalKeyframe);
3383
- options.onComplete();
3384
- });
3385
- return;
3386
- }
3387
- }
3388
- return valueTransition.isSync
3389
- ? new JSAnimation(options)
3390
- : new AsyncMotionValueAnimation(options);
3391
- };
3392
-
3393
- function getValueState(visualElement) {
3394
- const state = [{}, {}];
3395
- visualElement?.values.forEach((value, key) => {
3396
- state[0][key] = value.get();
3397
- state[1][key] = value.getVelocity();
3398
- });
3399
- return state;
3400
- }
3401
- function resolveVariantFromProps(props, definition, custom, visualElement) {
3402
- /**
3403
- * If the variant definition is a function, resolve.
3404
- */
3405
- if (typeof definition === "function") {
3406
- const [current, velocity] = getValueState(visualElement);
3407
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3408
- }
3409
- /**
3410
- * If the variant definition is a variant label, or
3411
- * the function returned a variant label, resolve.
3412
- */
3413
- if (typeof definition === "string") {
3414
- definition = props.variants && props.variants[definition];
3415
- }
3416
- /**
3417
- * At this point we've resolved both functions and variant labels,
3418
- * but the resolved variant label might itself have been a function.
3419
- * If so, resolve. This can only have returned a valid target object.
3420
- */
3421
- if (typeof definition === "function") {
3422
- const [current, velocity] = getValueState(visualElement);
3423
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3424
- }
3425
- return definition;
3426
- }
3427
-
3428
- function resolveVariant(visualElement, definition, custom) {
3429
- const props = visualElement.getProps();
3430
- return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3431
- }
3432
-
3433
- const positionalKeys = new Set([
3434
- "width",
3435
- "height",
3436
- "top",
3437
- "left",
3438
- "right",
3439
- "bottom",
3440
- ...transformPropOrder,
3441
- ]);
3442
-
3443
3185
  /**
3444
3186
  * Maximum time between the value of two frames, beyond which we
3445
3187
  * assume the velocity has since been 0.
@@ -3648,116 +3390,562 @@
3648
3390
  }
3649
3391
  }
3650
3392
  /**
3651
- * Returns the latest state of `MotionValue`
3652
- *
3653
- * @returns - The latest state of `MotionValue`
3654
- *
3655
- * @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.
3656
3623
  */
3657
- get() {
3658
- if (collectMotionValues.current) {
3659
- collectMotionValues.current.push(this);
3660
- }
3661
- return this.current;
3624
+ if (!isTransitionDefined(valueTransition)) {
3625
+ Object.assign(options, getDefaultTransition(name, options));
3662
3626
  }
3663
3627
  /**
3664
- * @public
3628
+ * Both WAAPI and our internal animation functions use durations
3629
+ * as defined by milliseconds, while our external API defines them
3630
+ * as seconds.
3665
3631
  */
3666
- getPrevious() {
3667
- return this.prev;
3668
- }
3632
+ options.duration && (options.duration = secondsToMilliseconds(options.duration));
3633
+ options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
3669
3634
  /**
3670
- * Returns the latest velocity of `MotionValue`
3671
- *
3672
- * @returns - The latest velocity of `MotionValue`. Returns `0` if the state is non-numerical.
3673
- *
3674
- * @public
3635
+ * Support deprecated way to set initial value. Prefer keyframe syntax.
3675
3636
  */
3676
- getVelocity() {
3677
- const currentTime = time.now();
3678
- if (!this.canTrackVelocity ||
3679
- this.prevFrameValue === undefined ||
3680
- currentTime - this.updatedAt > MAX_VELOCITY_DELTA) {
3681
- return 0;
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;
3682
3646
  }
3683
- const delta = Math.min(this.updatedAt - this.prevUpdatedAt, MAX_VELOCITY_DELTA);
3684
- // Casts because of parseFloat's poor typing
3685
- return velocityPerSecond(parseFloat(this.current) -
3686
- parseFloat(this.prevFrameValue), delta);
3647
+ }
3648
+ if (MotionGlobalConfig.instantAnimations ||
3649
+ MotionGlobalConfig.skipAnimations ||
3650
+ element?.shouldSkipAnimations ||
3651
+ valueTransition.skipAnimations) {
3652
+ shouldSkip = true;
3653
+ makeAnimationInstant(options);
3654
+ options.delay = 0;
3687
3655
  }
3688
3656
  /**
3689
- * Registers a new animation to control this `MotionValue`. Only one
3690
- * animation can drive a `MotionValue` at one time.
3691
- *
3692
- * ```jsx
3693
- * value.start()
3694
- * ```
3695
- *
3696
- * @param animation - A function that starts the provided animation
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.
3697
3659
  */
3698
- start(startAnimation) {
3699
- this.stop();
3700
- return new Promise((resolve) => {
3701
- this.hasAnimated = true;
3702
- this.animation = startAnimation(resolve);
3703
- if (this.events.animationStart) {
3704
- this.events.animationStart.notify();
3660
+ options.allowFlatten = !valueTransition.type && !valueTransition.ease;
3661
+ /**
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.
3665
+ */
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;
3674
+ }
3675
+ }
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;
3705
3766
  }
3706
- }).then(() => {
3707
- if (this.events.animationComplete) {
3708
- 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;
3709
3793
  }
3710
- this.clearAnimation();
3711
- });
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;
3712
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) {
3713
3908
  /**
3714
- * Stop the currently active animation.
3715
- *
3716
- * @public
3909
+ * If the variant definition is a function, resolve.
3717
3910
  */
3718
- stop() {
3719
- if (this.animation) {
3720
- this.animation.stop();
3721
- if (this.events.animationCancel) {
3722
- this.events.animationCancel.notify();
3723
- }
3724
- }
3725
- this.clearAnimation();
3911
+ if (typeof definition === "function") {
3912
+ const [current, velocity] = getValueState(visualElement);
3913
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3726
3914
  }
3727
3915
  /**
3728
- * Returns `true` if this value is currently animating.
3729
- *
3730
- * @public
3916
+ * If the variant definition is a variant label, or
3917
+ * the function returned a variant label, resolve.
3731
3918
  */
3732
- isAnimating() {
3733
- return !!this.animation;
3734
- }
3735
- clearAnimation() {
3736
- delete this.animation;
3919
+ if (typeof definition === "string") {
3920
+ definition = props.variants && props.variants[definition];
3737
3921
  }
3738
3922
  /**
3739
- * Destroy and clean up subscribers to this `MotionValue`.
3740
- *
3741
- * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
3742
- * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
3743
- * created a `MotionValue` via the `motionValue` function.
3744
- *
3745
- * @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.
3746
3926
  */
3747
- destroy() {
3748
- this.dependents?.clear();
3749
- this.events.destroy?.notify();
3750
- this.clearListeners();
3751
- this.stop();
3752
- if (this.stopPassiveEffect) {
3753
- this.stopPassiveEffect();
3754
- }
3927
+ if (typeof definition === "function") {
3928
+ const [current, velocity] = getValueState(visualElement);
3929
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3755
3930
  }
3931
+ return definition;
3756
3932
  }
3757
- function motionValue(init, options) {
3758
- 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);
3759
3937
  }
3760
3938
 
3939
+ const positionalKeys = new Set([
3940
+ "width",
3941
+ "height",
3942
+ "top",
3943
+ "left",
3944
+ "right",
3945
+ "bottom",
3946
+ ...transformPropOrder,
3947
+ ]);
3948
+
3761
3949
  const isKeyframesTarget = (v) => {
3762
3950
  return Array.isArray(v);
3763
3951
  };
@@ -3846,6 +4034,11 @@
3846
4034
  const animationTypeState = type &&
3847
4035
  visualElement.animationState &&
3848
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
+ }
3849
4042
  for (const key in target) {
3850
4043
  const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
3851
4044
  const valueTarget = target[key];
@@ -4064,6 +4257,12 @@
4064
4257
 
4065
4258
  const transformValueTypes = {
4066
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,
4067
4266
  rotateX: degrees,
4068
4267
  rotateY: degrees,
4069
4268
  rotateZ: degrees,
@@ -4615,6 +4814,15 @@
4615
4814
  transform += `${transformName}(${value}) `;
4616
4815
  }
4617
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
+ }
4618
4826
  return transformIsDefault ? "none" : transform.trim();
4619
4827
  }
4620
4828
 
@@ -6767,6 +6975,14 @@
6767
6975
  }
6768
6976
  }
6769
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
+ }
6770
6986
  transformString = transformString.trim();
6771
6987
  // If we have a custom `transform` template, pass our transform values and
6772
6988
  // generated transformString to that before returning
@@ -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;
@@ -11426,6 +11669,7 @@
11426
11669
  exports.applyPointDelta = applyPointDelta;
11427
11670
  exports.applyPxDefaults = applyPxDefaults;
11428
11671
  exports.applyTreeDeltas = applyTreeDeltas;
11672
+ exports.arc = arc;
11429
11673
  exports.aspectRatio = aspectRatio;
11430
11674
  exports.attachFollow = attachFollow;
11431
11675
  exports.attachSpring = attachSpring;