motion 12.39.0 → 12.41.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.
@@ -79,7 +79,7 @@
79
79
  }
80
80
 
81
81
  /*#__NO_SIDE_EFFECTS__*/
82
- const noop$1 = (any) => any;
82
+ const noop = (any) => any;
83
83
 
84
84
  /**
85
85
  * Pipe
@@ -216,7 +216,7 @@
216
216
  function cubicBezier(mX1, mY1, mX2, mY2) {
217
217
  // If this is a linear gradient, return linear easing
218
218
  if (mX1 === mY1 && mX2 === mY2)
219
- return noop$1;
219
+ return noop;
220
220
  const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
221
221
  // If animation is at start/end, return t without easing
222
222
  return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
@@ -277,7 +277,7 @@
277
277
  const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
278
278
 
279
279
  const easingLookup = {
280
- linear: noop$1,
280
+ linear: noop,
281
281
  easeIn,
282
282
  easeInOut,
283
283
  easeOut,
@@ -318,12 +318,7 @@
318
318
  "postRender", // Compute
319
319
  ];
320
320
 
321
- const statsBuffer = {
322
- value: null,
323
- addProjectionMetrics: null,
324
- };
325
-
326
- function createRenderStep(runNextFrame, stepName) {
321
+ function createRenderStep(runNextFrame) {
327
322
  /**
328
323
  * We create and reuse two queues, one to queue jobs for the current frame
329
324
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -345,13 +340,11 @@
345
340
  timestamp: 0.0,
346
341
  isProcessing: false,
347
342
  };
348
- let numCalls = 0;
349
343
  function triggerCallback(callback) {
350
344
  if (toKeepAlive.has(callback)) {
351
345
  step.schedule(callback);
352
346
  runNextFrame();
353
347
  }
354
- numCalls++;
355
348
  callback(latestFrameData);
356
349
  }
357
350
  const step = {
@@ -394,13 +387,6 @@
394
387
  nextFrame = prevFrame;
395
388
  // Execute this frame
396
389
  thisFrame.forEach(triggerCallback);
397
- /**
398
- * If we're recording stats then
399
- */
400
- if (stepName && statsBuffer.value) {
401
- statsBuffer.value.frameloop[stepName].push(numCalls);
402
- }
403
- numCalls = 0;
404
390
  // Clear the frame so no callbacks remain. This is to avoid
405
391
  // memory leaks should this render step not run for a while.
406
392
  thisFrame.clear();
@@ -425,7 +411,7 @@
425
411
  };
426
412
  const flagRunNextFrame = () => (runNextFrame = true);
427
413
  const steps = stepsOrder.reduce((acc, key) => {
428
- acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
414
+ acc[key] = createRenderStep(flagRunNextFrame);
429
415
  return acc;
430
416
  }, {});
431
417
  const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
@@ -481,7 +467,7 @@
481
467
  return { schedule, cancel, state, steps };
482
468
  }
483
469
 
484
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop$1, true);
470
+ const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
485
471
 
486
472
  let now;
487
473
  function clearTime() {
@@ -510,12 +496,6 @@
510
496
  },
511
497
  };
512
498
 
513
- const activeAnimations = {
514
- layout: 0,
515
- mainThread: 0,
516
- waapi: 0,
517
- };
518
-
519
499
  const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
520
500
  const isCSSVariableName =
521
501
  /*@__PURE__*/ checkStringStartsWith("--");
@@ -1503,7 +1483,7 @@
1503
1483
  for (let i = 0; i < numMixers; i++) {
1504
1484
  let mixer = mixerFactory(output[i], output[i + 1]);
1505
1485
  if (ease) {
1506
- const easingFunction = Array.isArray(ease) ? ease[i] || noop$1 : ease;
1486
+ const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
1507
1487
  mixer = pipe(easingFunction, mixer);
1508
1488
  }
1509
1489
  mixers.push(mixer);
@@ -1717,7 +1697,6 @@
1717
1697
  this.teardown();
1718
1698
  this.options.onStop?.();
1719
1699
  };
1720
- activeAnimations.mainThread++;
1721
1700
  this.options = options;
1722
1701
  this.initAnimation();
1723
1702
  this.play();
@@ -2022,7 +2001,6 @@
2022
2001
  this.state = "idle";
2023
2002
  this.stopDriver();
2024
2003
  this.startTime = this.holdTime = null;
2025
- activeAnimations.mainThread--;
2026
2004
  }
2027
2005
  stopDriver() {
2028
2006
  if (!this.driver)
@@ -2161,8 +2139,15 @@
2161
2139
  ];
2162
2140
  /**
2163
2141
  * A quick lookup for transform props.
2142
+ *
2143
+ * `pathRotation` is a transform for routing purposes (skipped from raw
2144
+ * style application, wired to the transform composite, flags transform
2145
+ * dirty) but is intentionally NOT in `transformPropOrder` — it is
2146
+ * composed onto `rotate` at the build sites, not serialized in its own
2147
+ * slot, and must stay out of the order-array consumers (parse-transform,
2148
+ * unit-conversion, keys-position).
2164
2149
  */
2165
- const transformProps = /*@__PURE__*/ (() => new Set(transformPropOrder))();
2150
+ const transformProps = /*@__PURE__*/ (() => new Set([...transformPropOrder, "pathRotation"]))();
2166
2151
 
2167
2152
  const isNumOrPxType = (v) => v === number || v === px;
2168
2153
  const transformKeys = new Set(["x", "y", "z"]);
@@ -2427,9 +2412,6 @@
2427
2412
  */
2428
2413
  if (Array.isArray(easing))
2429
2414
  keyframeOptions.easing = easing;
2430
- if (statsBuffer.value) {
2431
- activeAnimations.waapi++;
2432
- }
2433
2415
  const options = {
2434
2416
  delay,
2435
2417
  duration,
@@ -2440,13 +2422,7 @@
2440
2422
  };
2441
2423
  if (pseudoElement)
2442
2424
  options.pseudoElement = pseudoElement;
2443
- const animation = element.animate(keyframeOptions, options);
2444
- if (statsBuffer.value) {
2445
- animation.finished.finally(() => {
2446
- activeAnimations.waapi--;
2447
- });
2448
- }
2449
- return animation;
2425
+ return element.animate(keyframeOptions, options);
2450
2426
  }
2451
2427
 
2452
2428
  function isGenerator(type) {
@@ -2625,7 +2601,7 @@
2625
2601
  this.animation.rangeStart = rangeStart;
2626
2602
  if (rangeEnd)
2627
2603
  this.animation.rangeEnd = rangeEnd;
2628
- return noop$1;
2604
+ return noop;
2629
2605
  }
2630
2606
  else {
2631
2607
  return observe(this);
@@ -2978,7 +2954,7 @@
2978
2954
  }
2979
2955
  animation.finished.then(() => {
2980
2956
  this.notifyFinished();
2981
- }).catch(noop$1);
2957
+ }).catch(noop);
2982
2958
  if (this.pendingTimeline) {
2983
2959
  this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
2984
2960
  this.pendingTimeline = undefined;
@@ -3176,342 +3152,77 @@
3176
3152
  }
3177
3153
 
3178
3154
  /**
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.
3155
+ * Maximum time between the value of two frames, beyond which we
3156
+ * assume the velocity has since been 0.
3234
3157
  */
3235
- const ease = {
3236
- type: "keyframes",
3237
- ease: [0.25, 0.1, 0.35, 1],
3238
- duration: 0.3,
3158
+ const MAX_VELOCITY_DELTA = 30;
3159
+ const isFloat = (value) => {
3160
+ return !isNaN(parseFloat(value));
3239
3161
  };
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;
3162
+ const collectMotionValues = {
3163
+ current: undefined,
3250
3164
  };
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
3165
  /**
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.
3166
+ * `MotionValue` is used to track the state and velocity of motion values.
3167
+ *
3168
+ * @public
3291
3169
  */
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));
3170
+ class MotionValue {
3346
3171
  /**
3347
- * Support deprecated way to set initial value. Prefer keyframe syntax.
3172
+ * @param init - The initiating value
3173
+ * @param config - Optional configuration options
3174
+ *
3175
+ * - `transformer`: A function to transform incoming values with.
3348
3176
  */
3349
- if (options.from !== undefined) {
3350
- options.keyframes[0] = options.from;
3177
+ constructor(init, options = {}) {
3178
+ /**
3179
+ * Tracks whether this value can output a velocity. Currently this is only true
3180
+ * if the value is numerical, but we might be able to widen the scope here and support
3181
+ * other value types.
3182
+ *
3183
+ * @internal
3184
+ */
3185
+ this.canTrackVelocity = null;
3186
+ /**
3187
+ * An object containing a SubscriptionManager for each active event.
3188
+ */
3189
+ this.events = {};
3190
+ this.updateAndNotify = (v) => {
3191
+ const currentTime = time.now();
3192
+ /**
3193
+ * If we're updating the value during another frame or eventloop
3194
+ * than the previous frame, then the we set the previous frame value
3195
+ * to current.
3196
+ */
3197
+ if (this.updatedAt !== currentTime) {
3198
+ this.setPrevFrameValue();
3199
+ }
3200
+ this.prev = this.current;
3201
+ this.setCurrent(v);
3202
+ // Update update subscribers
3203
+ if (this.current !== this.prev) {
3204
+ this.events.change?.notify(this.current);
3205
+ if (this.dependents) {
3206
+ for (const dependent of this.dependents) {
3207
+ dependent.dirty();
3208
+ }
3209
+ }
3210
+ }
3211
+ };
3212
+ this.hasAnimated = false;
3213
+ this.setCurrent(init);
3214
+ this.owner = options.owner;
3351
3215
  }
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;
3216
+ setCurrent(current) {
3217
+ this.current = current;
3218
+ this.updatedAt = time.now();
3219
+ if (this.canTrackVelocity === null && current !== undefined) {
3220
+ this.canTrackVelocity = isFloat(this.current);
3358
3221
  }
3359
3222
  }
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
- /**
3444
- * Maximum time between the value of two frames, beyond which we
3445
- * assume the velocity has since been 0.
3446
- */
3447
- const MAX_VELOCITY_DELTA = 30;
3448
- const isFloat = (value) => {
3449
- return !isNaN(parseFloat(value));
3450
- };
3451
- const collectMotionValues = {
3452
- current: undefined,
3453
- };
3454
- /**
3455
- * `MotionValue` is used to track the state and velocity of motion values.
3456
- *
3457
- * @public
3458
- */
3459
- class MotionValue {
3460
- /**
3461
- * @param init - The initiating value
3462
- * @param config - Optional configuration options
3463
- *
3464
- * - `transformer`: A function to transform incoming values with.
3465
- */
3466
- constructor(init, options = {}) {
3467
- /**
3468
- * Tracks whether this value can output a velocity. Currently this is only true
3469
- * if the value is numerical, but we might be able to widen the scope here and support
3470
- * other value types.
3471
- *
3472
- * @internal
3473
- */
3474
- this.canTrackVelocity = null;
3475
- /**
3476
- * An object containing a SubscriptionManager for each active event.
3477
- */
3478
- this.events = {};
3479
- this.updateAndNotify = (v) => {
3480
- const currentTime = time.now();
3481
- /**
3482
- * If we're updating the value during another frame or eventloop
3483
- * than the previous frame, then the we set the previous frame value
3484
- * to current.
3485
- */
3486
- if (this.updatedAt !== currentTime) {
3487
- this.setPrevFrameValue();
3488
- }
3489
- this.prev = this.current;
3490
- this.setCurrent(v);
3491
- // Update update subscribers
3492
- if (this.current !== this.prev) {
3493
- this.events.change?.notify(this.current);
3494
- if (this.dependents) {
3495
- for (const dependent of this.dependents) {
3496
- dependent.dirty();
3497
- }
3498
- }
3499
- }
3500
- };
3501
- this.hasAnimated = false;
3502
- this.setCurrent(init);
3503
- this.owner = options.owner;
3504
- }
3505
- setCurrent(current) {
3506
- this.current = current;
3507
- this.updatedAt = time.now();
3508
- if (this.canTrackVelocity === null && current !== undefined) {
3509
- this.canTrackVelocity = isFloat(this.current);
3510
- }
3511
- }
3512
- setPrevFrameValue(prevFrameValue = this.current) {
3513
- this.prevFrameValue = prevFrameValue;
3514
- this.prevUpdatedAt = this.updatedAt;
3223
+ setPrevFrameValue(prevFrameValue = this.current) {
3224
+ this.prevFrameValue = prevFrameValue;
3225
+ this.prevUpdatedAt = this.updatedAt;
3515
3226
  }
3516
3227
  /**
3517
3228
  * Adds a function that will be notified when the `MotionValue` is updated.
@@ -3685,79 +3396,525 @@
3685
3396
  return velocityPerSecond(parseFloat(this.current) -
3686
3397
  parseFloat(this.prevFrameValue), delta);
3687
3398
  }
3688
- /**
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
3697
- */
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();
3399
+ /**
3400
+ * Registers a new animation to control this `MotionValue`. Only one
3401
+ * animation can drive a `MotionValue` at one time.
3402
+ *
3403
+ * ```jsx
3404
+ * value.start()
3405
+ * ```
3406
+ *
3407
+ * @param animation - A function that starts the provided animation
3408
+ */
3409
+ start(startAnimation) {
3410
+ this.stop();
3411
+ return new Promise((resolve) => {
3412
+ this.hasAnimated = true;
3413
+ this.animation = startAnimation(resolve);
3414
+ if (this.events.animationStart) {
3415
+ this.events.animationStart.notify();
3416
+ }
3417
+ }).then(() => {
3418
+ if (this.events.animationComplete) {
3419
+ this.events.animationComplete.notify();
3420
+ }
3421
+ this.clearAnimation();
3422
+ });
3423
+ }
3424
+ /**
3425
+ * Stop the currently active animation.
3426
+ *
3427
+ * @public
3428
+ */
3429
+ stop() {
3430
+ if (this.animation) {
3431
+ this.animation.stop();
3432
+ if (this.events.animationCancel) {
3433
+ this.events.animationCancel.notify();
3434
+ }
3435
+ }
3436
+ this.clearAnimation();
3437
+ }
3438
+ /**
3439
+ * Returns `true` if this value is currently animating.
3440
+ *
3441
+ * @public
3442
+ */
3443
+ isAnimating() {
3444
+ return !!this.animation;
3445
+ }
3446
+ clearAnimation() {
3447
+ delete this.animation;
3448
+ }
3449
+ /**
3450
+ * Destroy and clean up subscribers to this `MotionValue`.
3451
+ *
3452
+ * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
3453
+ * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
3454
+ * created a `MotionValue` via the `motionValue` function.
3455
+ *
3456
+ * @public
3457
+ */
3458
+ destroy() {
3459
+ this.dependents?.clear();
3460
+ this.events.destroy?.notify();
3461
+ this.clearListeners();
3462
+ this.stop();
3463
+ if (this.stopPassiveEffect) {
3464
+ this.stopPassiveEffect();
3465
+ }
3466
+ }
3467
+ }
3468
+ function motionValue(init, options) {
3469
+ return new MotionValue(init, options);
3470
+ }
3471
+
3472
+ /**
3473
+ * If `transition` has `inherit: true`, shallow-merge it with
3474
+ * `parentTransition` (child keys win) and strip the `inherit` key.
3475
+ * Otherwise return `transition` unchanged.
3476
+ */
3477
+ function resolveTransition(transition, parentTransition) {
3478
+ if (transition?.inherit && parentTransition) {
3479
+ const { inherit: _, ...rest } = transition;
3480
+ return { ...parentTransition, ...rest };
3481
+ }
3482
+ return transition;
3483
+ }
3484
+
3485
+ function getValueTransition$1(transition, key) {
3486
+ const valueTransition = transition?.[key] ??
3487
+ transition?.["default"] ??
3488
+ transition;
3489
+ if (valueTransition !== transition) {
3490
+ return resolveTransition(valueTransition, transition);
3491
+ }
3492
+ return valueTransition;
3493
+ }
3494
+
3495
+ const underDampedSpring = {
3496
+ type: "spring",
3497
+ stiffness: 500,
3498
+ damping: 25,
3499
+ restSpeed: 10,
3500
+ };
3501
+ const criticallyDampedSpring = (target) => ({
3502
+ type: "spring",
3503
+ stiffness: 550,
3504
+ damping: target === 0 ? 2 * Math.sqrt(550) : 30,
3505
+ restSpeed: 10,
3506
+ });
3507
+ const keyframesTransition = {
3508
+ type: "keyframes",
3509
+ duration: 0.8,
3510
+ };
3511
+ /**
3512
+ * Default easing curve is a slightly shallower version of
3513
+ * the default browser easing curve.
3514
+ */
3515
+ const ease = {
3516
+ type: "keyframes",
3517
+ ease: [0.25, 0.1, 0.35, 1],
3518
+ duration: 0.3,
3519
+ };
3520
+ const getDefaultTransition = (valueKey, { keyframes }) => {
3521
+ if (keyframes.length > 2) {
3522
+ return keyframesTransition;
3523
+ }
3524
+ else if (transformProps.has(valueKey)) {
3525
+ return valueKey.startsWith("scale")
3526
+ ? criticallyDampedSpring(keyframes[1])
3527
+ : underDampedSpring;
3528
+ }
3529
+ return ease;
3530
+ };
3531
+
3532
+ const orchestrationKeys = new Set([
3533
+ "when",
3534
+ "delay",
3535
+ "delayChildren",
3536
+ "staggerChildren",
3537
+ "staggerDirection",
3538
+ "repeat",
3539
+ "repeatType",
3540
+ "repeatDelay",
3541
+ "from",
3542
+ "elapsed",
3543
+ ]);
3544
+ /**
3545
+ * Decide whether a transition is defined on a given Transition.
3546
+ * This filters out orchestration options and returns true
3547
+ * if any options are left.
3548
+ */
3549
+ function isTransitionDefined(transition) {
3550
+ for (const key in transition) {
3551
+ if (!orchestrationKeys.has(key))
3552
+ return true;
3553
+ }
3554
+ return false;
3555
+ }
3556
+
3557
+ const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
3558
+ const valueTransition = getValueTransition$1(transition, name) || {};
3559
+ /**
3560
+ * Most transition values are currently completely overwritten by value-specific
3561
+ * transitions. In the future it'd be nicer to blend these transitions. But for now
3562
+ * delay actually does inherit from the root transition if not value-specific.
3563
+ */
3564
+ const delay = valueTransition.delay || transition.delay || 0;
3565
+ /**
3566
+ * Elapsed isn't a public transition option but can be passed through from
3567
+ * optimized appear effects in milliseconds.
3568
+ */
3569
+ let { elapsed = 0 } = transition;
3570
+ elapsed = elapsed - secondsToMilliseconds(delay);
3571
+ const options = {
3572
+ keyframes: Array.isArray(target) ? target : [null, target],
3573
+ ease: "easeOut",
3574
+ velocity: value.getVelocity(),
3575
+ ...valueTransition,
3576
+ delay: -elapsed,
3577
+ onUpdate: (v) => {
3578
+ value.set(v);
3579
+ valueTransition.onUpdate && valueTransition.onUpdate(v);
3580
+ },
3581
+ onComplete: () => {
3582
+ onComplete();
3583
+ valueTransition.onComplete && valueTransition.onComplete();
3584
+ },
3585
+ name,
3586
+ motionValue: value,
3587
+ element: isHandoff ? undefined : element,
3588
+ };
3589
+ /**
3590
+ * If there's no transition defined for this value, we can generate
3591
+ * unique transition settings for this value.
3592
+ */
3593
+ if (!isTransitionDefined(valueTransition)) {
3594
+ Object.assign(options, getDefaultTransition(name, options));
3595
+ }
3596
+ /**
3597
+ * Both WAAPI and our internal animation functions use durations
3598
+ * as defined by milliseconds, while our external API defines them
3599
+ * as seconds.
3600
+ */
3601
+ options.duration && (options.duration = secondsToMilliseconds(options.duration));
3602
+ options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
3603
+ /**
3604
+ * Support deprecated way to set initial value. Prefer keyframe syntax.
3605
+ */
3606
+ if (options.from !== undefined) {
3607
+ options.keyframes[0] = options.from;
3608
+ }
3609
+ let shouldSkip = false;
3610
+ if (options.type === false ||
3611
+ (options.duration === 0 && !options.repeatDelay)) {
3612
+ makeAnimationInstant(options);
3613
+ if (options.delay === 0) {
3614
+ shouldSkip = true;
3615
+ }
3616
+ }
3617
+ if (MotionGlobalConfig.instantAnimations ||
3618
+ MotionGlobalConfig.skipAnimations ||
3619
+ element?.shouldSkipAnimations ||
3620
+ valueTransition.skipAnimations) {
3621
+ shouldSkip = true;
3622
+ makeAnimationInstant(options);
3623
+ options.delay = 0;
3624
+ }
3625
+ /**
3626
+ * If the transition type or easing has been explicitly set by the user
3627
+ * then we don't want to allow flattening the animation.
3628
+ */
3629
+ options.allowFlatten = !valueTransition.type && !valueTransition.ease;
3630
+ /**
3631
+ * If we can or must skip creating the animation, and apply only
3632
+ * the final keyframe, do so. We also check once keyframes are resolved but
3633
+ * this early check prevents the need to create an animation at all.
3634
+ */
3635
+ if (shouldSkip && !isHandoff && value.get() !== undefined) {
3636
+ const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
3637
+ if (finalKeyframe !== undefined) {
3638
+ frame.update(() => {
3639
+ options.onUpdate(finalKeyframe);
3640
+ options.onComplete();
3641
+ });
3642
+ return;
3643
+ }
3644
+ }
3645
+ return valueTransition.isSync
3646
+ ? new JSAnimation(options)
3647
+ : new AsyncMotionValueAnimation(options);
3648
+ };
3649
+
3650
+ const MIN_LAYOUT_DISTANCE = 20;
3651
+ function bezierPoint(t, origin, control, target) {
3652
+ const inv = 1 - t;
3653
+ return inv * inv * origin + 2 * inv * t * control + t * t * target;
3654
+ }
3655
+ function bezierTangentAngle(t, originX, controlX, targetX, originY, controlY, targetY) {
3656
+ const dx = 2 * (1 - t) * (controlX - originX) + 2 * t * (targetX - controlX);
3657
+ const dy = 2 * (1 - t) * (controlY - originY) + 2 * t * (targetY - controlY);
3658
+ return Math.atan2(dy, dx) * (180 / Math.PI);
3659
+ }
3660
+ function computeArcControlPoint(fromX, fromY, toX, toY, strength, peak) {
3661
+ const deltaX = toX - fromX;
3662
+ const deltaY = toY - fromY;
3663
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
3664
+ if (distance > 0) {
3665
+ const normalPerpX = -deltaY / distance;
3666
+ const normalPerpY = deltaX / distance;
3667
+ const desiredHeight = strength * distance;
3668
+ return {
3669
+ x: fromX + deltaX * peak + normalPerpX * desiredHeight,
3670
+ y: fromY + deltaY * peak + normalPerpY * desiredHeight,
3671
+ };
3672
+ }
3673
+ return { x: fromX, y: fromY };
3674
+ }
3675
+ /**
3676
+ * The pure sampling factory: `(from, to) => (t) => point`. Internal —
3677
+ * used by {@link arc} and the unit tests. Not part of the public surface.
3678
+ */
3679
+ function createArcPath({ strength = 0.5, peak = 0.5, direction, rotate = false, } = {}) {
3680
+ const rotationScale = rotate === true ? 1 : typeof rotate === "number" ? rotate : 0;
3681
+ // Auto-direction only: persists across calls to flip the bulge back
3682
+ // onto the same screen side when the dominant axis changes between
3683
+ // calls. Reuse the factory (module scope / useMemo) to keep this alive.
3684
+ let prevBulgeSign;
3685
+ const createInterpolator = (from, to) => {
3686
+ const dx = to.x - from.x;
3687
+ const dy = to.y - from.y;
3688
+ let signed;
3689
+ if (direction === "cw") {
3690
+ signed = -strength;
3691
+ }
3692
+ else if (direction === "ccw") {
3693
+ signed = strength;
3694
+ }
3695
+ else {
3696
+ const dom = Math.abs(dx) >= Math.abs(dy) ? dx : dy;
3697
+ signed = dom < 0 ? -strength : strength;
3698
+ }
3699
+ let control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
3700
+ if (direction === undefined) {
3701
+ const isVertical = Math.abs(dx) < Math.abs(dy);
3702
+ const midX = from.x + dx * peak;
3703
+ const midY = from.y + dy * peak;
3704
+ const bulgeSign = isVertical
3705
+ ? Math.sign(control.x - midX)
3706
+ : Math.sign(control.y - midY);
3707
+ if (prevBulgeSign !== undefined &&
3708
+ bulgeSign !== 0 &&
3709
+ bulgeSign !== prevBulgeSign) {
3710
+ signed = -signed;
3711
+ control = computeArcControlPoint(from.x, from.y, to.x, to.y, signed, peak);
3712
+ }
3713
+ else if (bulgeSign !== 0) {
3714
+ prevBulgeSign = bulgeSign;
3715
+ }
3716
+ }
3717
+ const tangent0 = rotationScale
3718
+ ? bezierTangentAngle(0, from.x, control.x, to.x, from.y, control.y, to.y)
3719
+ : 0;
3720
+ const tangent1 = rotationScale
3721
+ ? bezierTangentAngle(1, from.x, control.x, to.x, from.y, control.y, to.y)
3722
+ : 0;
3723
+ const tangentDelta = rotationScale
3724
+ ? wrap(-180, 180, tangent1 - tangent0)
3725
+ : 0;
3726
+ return (t) => {
3727
+ const out = {
3728
+ x: bezierPoint(t, from.x, control.x, to.x),
3729
+ y: bezierPoint(t, from.y, control.y, to.y),
3730
+ };
3731
+ if (rotationScale) {
3732
+ const raw = bezierTangentAngle(t, from.x, control.x, to.x, from.y, control.y, to.y);
3733
+ const baseline = tangent0 + tangentDelta * t;
3734
+ out.rotate = wrap(-180, 180, raw - baseline) * rotationScale;
3705
3735
  }
3706
- }).then(() => {
3707
- if (this.events.animationComplete) {
3708
- this.events.animationComplete.notify();
3736
+ return out;
3737
+ };
3738
+ };
3739
+ return createInterpolator;
3740
+ }
3741
+ /**
3742
+ * Creates a curved path for `transition.path`:
3743
+ *
3744
+ * ```ts
3745
+ * <motion.div animate={{ x: 200, y: 100 }} transition={{ path: arc() }} />
3746
+ * ```
3747
+ *
3748
+ * Reuse the returned value (module scope / useMemo / useRef) so its
3749
+ * continuity closure survives re-renders — a fresh `arc()` has no memory.
3750
+ */
3751
+ function arc(options = {}) {
3752
+ const sample = createArcPath(options);
3753
+ const path = {
3754
+ interpolateProjection(delta) {
3755
+ // `from` is the current translate offset (carries any in-flight
3756
+ // displacement when interrupted); `to` is the new layout origin.
3757
+ // The distance floor avoids visible wobble on tiny shifts.
3758
+ const tx = delta.x.translate;
3759
+ const ty = delta.y.translate;
3760
+ if (Math.sqrt(tx * tx + ty * ty) < MIN_LAYOUT_DISTANCE) {
3761
+ return undefined;
3709
3762
  }
3710
- this.clearAnimation();
3711
- });
3763
+ return sample({ x: tx, y: ty }, { x: 0, y: 0 });
3764
+ },
3765
+ animateVisualElement(visualElement, target, transition, delay, animations) {
3766
+ if (!("x" in target || "y" in target))
3767
+ return;
3768
+ const xValue = visualElement.getValue("x", visualElement.latestValues["x"] ?? 0);
3769
+ const yValue = visualElement.getValue("y", visualElement.latestValues["y"] ?? 0);
3770
+ const xRaw = target.x;
3771
+ const yRaw = target.y;
3772
+ const xFrom = (Array.isArray(xRaw) && xRaw[0] != null
3773
+ ? xRaw[0]
3774
+ : xValue?.get()) ?? 0;
3775
+ const yFrom = (Array.isArray(yRaw) && yRaw[0] != null
3776
+ ? yRaw[0]
3777
+ : yValue?.get()) ?? 0;
3778
+ const xTo = (Array.isArray(xRaw)
3779
+ ? xRaw[xRaw.length - 1]
3780
+ : xRaw ?? xFrom);
3781
+ const yTo = (Array.isArray(yRaw)
3782
+ ? yRaw[yRaw.length - 1]
3783
+ : yRaw ?? yFrom);
3784
+ // Interruption needs no flag: x/y already hold the displaced
3785
+ // mid-arc position, so xFrom/yFrom carry the continuity geometry.
3786
+ const interpolate = sample({ x: xFrom, y: yFrom }, { x: xTo, y: yTo });
3787
+ // Drive a dedicated `pathRotation` value (composed onto `rotate`
3788
+ // at the build sites) rather than `rotate` itself, so a
3789
+ // concurrent rotate animation composes and nothing accumulates
3790
+ // on interrupt.
3791
+ const pathRotationValue = interpolate(0).rotate !== undefined
3792
+ ? visualElement.getValue("pathRotation", 0)
3793
+ : undefined;
3794
+ const pathTransition = {
3795
+ delay,
3796
+ ...getValueTransition$1(transition || {}, "x"),
3797
+ };
3798
+ delete pathTransition.path;
3799
+ const progress = motionValue(0);
3800
+ progress.start(animateMotionValue("", progress, [0, 1000], {
3801
+ ...pathTransition,
3802
+ isSync: true,
3803
+ velocity: 0,
3804
+ onUpdate: (latest) => {
3805
+ const point = interpolate(latest / 1000);
3806
+ xValue?.set(point.x);
3807
+ yValue?.set(point.y);
3808
+ if (pathRotationValue && point.rotate !== undefined) {
3809
+ pathRotationValue.set(point.rotate);
3810
+ }
3811
+ },
3812
+ onComplete: () => {
3813
+ xValue?.set(xTo);
3814
+ yValue?.set(yTo);
3815
+ pathRotationValue?.set(0);
3816
+ },
3817
+ // Interrupt/cancel must clear our additive contribution
3818
+ // so it can't linger on top of the user's `rotate`.
3819
+ onStop: () => pathRotationValue?.set(0),
3820
+ onCancel: () => pathRotationValue?.set(0),
3821
+ }));
3822
+ if (progress.animation)
3823
+ animations.push(progress.animation);
3824
+ delete target.x;
3825
+ delete target.y;
3826
+ },
3827
+ };
3828
+ return path;
3829
+ }
3830
+
3831
+ /**
3832
+ * Parse Framer's special CSS variable format into a CSS token and a fallback.
3833
+ *
3834
+ * ```
3835
+ * `var(--foo, #fff)` => [`--foo`, '#fff']
3836
+ * ```
3837
+ *
3838
+ * @param current
3839
+ */
3840
+ const splitCSSVariableRegex =
3841
+ // eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive, as it can match a lot of words
3842
+ /^var\(--(?:([\w-]+)|([\w-]+), ?([a-zA-Z\d ()%#.,-]+))\)/u;
3843
+ function parseCSSVariable(current) {
3844
+ const match = splitCSSVariableRegex.exec(current);
3845
+ if (!match)
3846
+ return [,];
3847
+ const [, token1, token2, fallback] = match;
3848
+ return [`--${token1 ?? token2}`, fallback];
3849
+ }
3850
+ const maxDepth = 4;
3851
+ function getVariableValue(current, element, depth = 1) {
3852
+ exports.invariant(depth <= maxDepth, `Max CSS variable fallback depth detected in property "${current}". This may indicate a circular fallback dependency.`, "max-css-var-depth");
3853
+ const [token, fallback] = parseCSSVariable(current);
3854
+ // No CSS variable detected
3855
+ if (!token)
3856
+ return;
3857
+ // Attempt to read this CSS variable off the element
3858
+ const resolved = window.getComputedStyle(element).getPropertyValue(token);
3859
+ if (resolved) {
3860
+ const trimmed = resolved.trim();
3861
+ return isNumericalString(trimmed) ? parseFloat(trimmed) : trimmed;
3712
3862
  }
3863
+ return isCSSVariableToken(fallback)
3864
+ ? getVariableValue(fallback, element, depth + 1)
3865
+ : fallback;
3866
+ }
3867
+
3868
+ function getValueState(visualElement) {
3869
+ const state = [{}, {}];
3870
+ visualElement?.values.forEach((value, key) => {
3871
+ state[0][key] = value.get();
3872
+ state[1][key] = value.getVelocity();
3873
+ });
3874
+ return state;
3875
+ }
3876
+ function resolveVariantFromProps(props, definition, custom, visualElement) {
3713
3877
  /**
3714
- * Stop the currently active animation.
3715
- *
3716
- * @public
3878
+ * If the variant definition is a function, resolve.
3717
3879
  */
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();
3880
+ if (typeof definition === "function") {
3881
+ const [current, velocity] = getValueState(visualElement);
3882
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3726
3883
  }
3727
3884
  /**
3728
- * Returns `true` if this value is currently animating.
3729
- *
3730
- * @public
3885
+ * If the variant definition is a variant label, or
3886
+ * the function returned a variant label, resolve.
3731
3887
  */
3732
- isAnimating() {
3733
- return !!this.animation;
3734
- }
3735
- clearAnimation() {
3736
- delete this.animation;
3888
+ if (typeof definition === "string") {
3889
+ definition = props.variants && props.variants[definition];
3737
3890
  }
3738
3891
  /**
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
3892
+ * At this point we've resolved both functions and variant labels,
3893
+ * but the resolved variant label might itself have been a function.
3894
+ * If so, resolve. This can only have returned a valid target object.
3746
3895
  */
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
- }
3896
+ if (typeof definition === "function") {
3897
+ const [current, velocity] = getValueState(visualElement);
3898
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3755
3899
  }
3900
+ return definition;
3756
3901
  }
3757
- function motionValue(init, options) {
3758
- return new MotionValue(init, options);
3902
+
3903
+ function resolveVariant(visualElement, definition, custom) {
3904
+ const props = visualElement.getProps();
3905
+ return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3759
3906
  }
3760
3907
 
3908
+ const positionalKeys = new Set([
3909
+ "width",
3910
+ "height",
3911
+ "top",
3912
+ "left",
3913
+ "right",
3914
+ "bottom",
3915
+ ...transformPropOrder,
3916
+ ]);
3917
+
3761
3918
  const isKeyframesTarget = (v) => {
3762
3919
  return Array.isArray(v);
3763
3920
  };
@@ -3846,6 +4003,11 @@
3846
4003
  const animationTypeState = type &&
3847
4004
  visualElement.animationState &&
3848
4005
  visualElement.animationState.getState()[type];
4006
+ const path = transition?.path;
4007
+ if (path) {
4008
+ // path mutates `target` to claim x/y; loop below skips them.
4009
+ path.animateVisualElement(visualElement, target, transition, delay, animations);
4010
+ }
3849
4011
  for (const key in target) {
3850
4012
  const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
3851
4013
  const valueTarget = target[key];
@@ -4064,6 +4226,12 @@
4064
4226
 
4065
4227
  const transformValueTypes = {
4066
4228
  rotate: degrees,
4229
+ /**
4230
+ * Internal channel for `transition.path` orientToPath. Composed onto
4231
+ * `rotate` at the transform-build sites so the user's `rotate` is
4232
+ * never read or overwritten. Not part of `transformPropOrder`.
4233
+ */
4234
+ pathRotation: degrees,
4067
4235
  rotateX: degrees,
4068
4236
  rotateY: degrees,
4069
4237
  rotateZ: degrees,
@@ -4615,6 +4783,15 @@
4615
4783
  transform += `${transformName}(${value}) `;
4616
4784
  }
4617
4785
  }
4786
+ // See build-transform.ts: additive `rotate()` so user `rotate` isn't
4787
+ // clobbered. Not a `transformPropOrder` slot.
4788
+ const pathRotation = state.latest.pathRotation;
4789
+ if (pathRotation) {
4790
+ transformIsDefault = false;
4791
+ transform += `rotate(${typeof pathRotation === "number"
4792
+ ? `${pathRotation}deg`
4793
+ : pathRotation}) `;
4794
+ }
4618
4795
  return transformIsDefault ? "none" : transform.trim();
4619
4796
  }
4620
4797
 
@@ -4955,9 +5132,16 @@
4955
5132
  claimedPointerDownEvents.add(startEvent);
4956
5133
  }
4957
5134
  const onPressEnd = onPressStart(target, startEvent);
5135
+ /**
5136
+ * End listeners run in the capture phase so a descendant calling
5137
+ * stopPropagation() in its own pointerup handler can't prevent the
5138
+ * press gesture from ending. This also keeps the gesture-end
5139
+ * ordering consistent with the drag gesture. See #2794.
5140
+ */
5141
+ const endEventOptions = { ...eventOptions, capture: true };
4958
5142
  const onPointerEnd = (endEvent, success) => {
4959
- window.removeEventListener("pointerup", onPointerUp);
4960
- window.removeEventListener("pointercancel", onPointerCancel);
5143
+ window.removeEventListener("pointerup", onPointerUp, endEventOptions);
5144
+ window.removeEventListener("pointercancel", onPointerCancel, endEventOptions);
4961
5145
  if (isPressing.has(target)) {
4962
5146
  isPressing.delete(target);
4963
5147
  }
@@ -4977,8 +5161,8 @@
4977
5161
  const onPointerCancel = (cancelEvent) => {
4978
5162
  onPointerEnd(cancelEvent, false);
4979
5163
  };
4980
- window.addEventListener("pointerup", onPointerUp, eventOptions);
4981
- window.addEventListener("pointercancel", onPointerCancel, eventOptions);
5164
+ window.addEventListener("pointerup", onPointerUp, endEventOptions);
5165
+ window.addEventListener("pointercancel", onPointerCancel, endEventOptions);
4982
5166
  };
4983
5167
  targets.forEach((target) => {
4984
5168
  const pointerDownTarget = options.useGlobalTarget ? window : target;
@@ -5117,116 +5301,34 @@
5117
5301
  return () => cancelFrame(onFrame);
5118
5302
  }
5119
5303
 
5120
- function record() {
5121
- const { value } = statsBuffer;
5122
- if (value === null) {
5123
- cancelFrame(record);
5124
- return;
5125
- }
5126
- value.frameloop.rate.push(frameData.delta);
5127
- value.animations.mainThread.push(activeAnimations.mainThread);
5128
- value.animations.waapi.push(activeAnimations.waapi);
5129
- value.animations.layout.push(activeAnimations.layout);
5130
- }
5131
- function mean(values) {
5132
- return values.reduce((acc, value) => acc + value, 0) / values.length;
5133
- }
5134
- function summarise(values, calcAverage = mean) {
5135
- if (values.length === 0) {
5136
- return {
5137
- min: 0,
5138
- max: 0,
5139
- avg: 0,
5140
- };
5141
- }
5142
- return {
5143
- min: Math.min(...values),
5144
- max: Math.max(...values),
5145
- avg: calcAverage(values),
5146
- };
5147
- }
5148
- const msToFps = (ms) => Math.round(1000 / ms);
5304
+ const statsBuffer = {
5305
+ value: null,
5306
+ addProjectionMetrics: null,
5307
+ };
5308
+
5149
5309
  function clearStatsBuffer() {
5150
5310
  statsBuffer.value = null;
5151
5311
  statsBuffer.addProjectionMetrics = null;
5152
5312
  }
5153
- function reportStats() {
5154
- const { value } = statsBuffer;
5155
- if (!value) {
5156
- throw new Error("Stats are not being measured");
5157
- }
5158
- clearStatsBuffer();
5159
- cancelFrame(record);
5160
- const summary = {
5161
- frameloop: {
5162
- setup: summarise(value.frameloop.setup),
5163
- rate: summarise(value.frameloop.rate),
5164
- read: summarise(value.frameloop.read),
5165
- resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
5166
- preUpdate: summarise(value.frameloop.preUpdate),
5167
- update: summarise(value.frameloop.update),
5168
- preRender: summarise(value.frameloop.preRender),
5169
- render: summarise(value.frameloop.render),
5170
- postRender: summarise(value.frameloop.postRender),
5171
- },
5172
- animations: {
5173
- mainThread: summarise(value.animations.mainThread),
5174
- waapi: summarise(value.animations.waapi),
5175
- layout: summarise(value.animations.layout),
5176
- },
5177
- layoutProjection: {
5178
- nodes: summarise(value.layoutProjection.nodes),
5179
- calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
5180
- calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
5181
- },
5182
- };
5183
- /**
5184
- * Convert the rate to FPS
5185
- */
5186
- const { rate } = summary.frameloop;
5187
- rate.min = msToFps(rate.min);
5188
- rate.max = msToFps(rate.max);
5189
- rate.avg = msToFps(rate.avg);
5190
- [rate.min, rate.max] = [rate.max, rate.min];
5191
- return summary;
5192
- }
5193
5313
  function recordStats() {
5194
5314
  if (statsBuffer.value) {
5195
5315
  clearStatsBuffer();
5196
5316
  throw new Error("Stats are already being measured");
5197
5317
  }
5198
- const newStatsBuffer = statsBuffer;
5199
- newStatsBuffer.value = {
5200
- frameloop: {
5201
- setup: [],
5202
- rate: [],
5203
- read: [],
5204
- resolveKeyframes: [],
5205
- preUpdate: [],
5206
- update: [],
5207
- preRender: [],
5208
- render: [],
5209
- postRender: [],
5210
- },
5211
- animations: {
5212
- mainThread: [],
5213
- waapi: [],
5214
- layout: [],
5215
- },
5318
+ const buffer = statsBuffer;
5319
+ buffer.value = {
5216
5320
  layoutProjection: {
5217
5321
  nodes: [],
5218
5322
  calculatedTargetDeltas: [],
5219
5323
  calculatedProjections: [],
5220
5324
  },
5221
5325
  };
5222
- newStatsBuffer.addProjectionMetrics = (metrics) => {
5223
- const { layoutProjection } = newStatsBuffer.value;
5326
+ buffer.addProjectionMetrics = (metrics) => {
5327
+ const { layoutProjection } = buffer.value;
5224
5328
  layoutProjection.nodes.push(metrics.nodes);
5225
5329
  layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
5226
5330
  layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
5227
5331
  };
5228
- frame.postRender(record, true);
5229
- return reportStats;
5230
5332
  }
5231
5333
 
5232
5334
  /**
@@ -5512,14 +5614,126 @@
5512
5614
  */
5513
5615
  const findValueType = (v) => valueTypes.find(testValueType(v));
5514
5616
 
5617
+ let nameCount = 0;
5618
+ /**
5619
+ * Generated names live in their own namespace so we can tell a name we own
5620
+ * (and must clean up) from an author-defined one - and so a stale generated
5621
+ * name left behind by an interrupted transition is re-owned, not mistaken for
5622
+ * the author's and leaked.
5623
+ */
5624
+ const generatedName = () => `motion-view-${nameCount++}`;
5625
+ const isGeneratedName = (name) => name.startsWith("motion-view-");
5626
+ /**
5627
+ * Tag a captured element with a `view-transition-class` so authors can target
5628
+ * its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
5629
+ * the opaque generated name. Tracked in `classed` - separate from the generated
5630
+ * names in `assigned` - so cleanup removes the class without ever stripping an
5631
+ * author's own inline `view-transition-name`.
5632
+ */
5633
+ function tagClass(element, className, classed) {
5634
+ if (!className)
5635
+ return;
5636
+ element.style?.setProperty("view-transition-class", className);
5637
+ classed.push(element);
5638
+ }
5639
+ /**
5640
+ * Resolve a selector/Element to elements and ensure each one carries a
5641
+ * `view-transition-name` we can target from script.
5642
+ *
5643
+ * Author-defined names are reused as-is. Elements that are unnamed (or use
5644
+ * the browser's `auto`/`match-element`, whose generated name is not exposed
5645
+ * to script) are given a unique generated name, set inline so it's captured,
5646
+ * and tracked in `assigned` for later cleanup.
5647
+ *
5648
+ * `registry` maps each Element to its name so the same element keeps its name
5649
+ * across both captures (before and after the update), which is what allows a
5650
+ * persistent element to animate as a single `group` layer.
5651
+ */
5652
+ function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = []) {
5653
+ const elements = resolveElements(definition);
5654
+ /**
5655
+ * The new end of a paired morph: give each element the matching name from
5656
+ * the old end (by index) so the two share one layer and morph. If the new
5657
+ * end resolves to *more* elements than the old end named, the extras have no
5658
+ * counterpart - give them a fresh name so they animate as newcomers rather
5659
+ * than being silently left unnamed. We return the names actually assigned
5660
+ * (sized to the resolved elements), not the raw `forcedNames`, so stagger
5661
+ * totals and the layer set stay in step with what's on the page.
5662
+ */
5663
+ if (forcedNames) {
5664
+ return elements.map((element, i) => {
5665
+ const existing = registry.get(element);
5666
+ if (existing)
5667
+ return existing;
5668
+ const name = forcedNames[i] ?? generatedName();
5669
+ element.style?.setProperty("view-transition-name", name);
5670
+ assigned.push(element);
5671
+ registry.set(element, name);
5672
+ tagClass(element, className, classed);
5673
+ return name;
5674
+ });
5675
+ }
5676
+ /**
5677
+ * Read every current name up front, before assigning any. Interleaving the
5678
+ * reads with the inline `setProperty` writes below would dirty styles
5679
+ * between reads and force a style recalc per element; batching the reads
5680
+ * keeps it to one. Elements already in the registry keep their name and
5681
+ * need no read.
5682
+ */
5683
+ const currentNames = elements.map((element) => registry.has(element)
5684
+ ? undefined
5685
+ : getComputedStyle(element).getPropertyValue("view-transition-name"));
5686
+ return elements.map((element, i) => {
5687
+ const existing = registry.get(element);
5688
+ if (existing)
5689
+ return existing;
5690
+ const current = currentNames[i];
5691
+ let name;
5692
+ if (current &&
5693
+ current !== "none" &&
5694
+ current !== "auto" &&
5695
+ current !== "match-element" &&
5696
+ !isGeneratedName(current)) {
5697
+ /**
5698
+ * The author already named this layer - target it as-is and leave
5699
+ * it to them to clean up. `auto`/`match-element` are overridden
5700
+ * because their generated name is not exposed to script, and a
5701
+ * stale `motion-view-*` (e.g. left by an interrupted transition) is
5702
+ * re-owned rather than adopted as an author name and leaked.
5703
+ */
5704
+ name = current;
5705
+ }
5706
+ else {
5707
+ name = generatedName();
5708
+ element.style?.setProperty("view-transition-name", name);
5709
+ assigned.push(element);
5710
+ }
5711
+ registry.set(element, name);
5712
+ tagClass(element, className, classed);
5713
+ return name;
5714
+ });
5715
+ }
5716
+ /**
5717
+ * Remove the `view-transition-name`s we generated and the
5718
+ * `view-transition-class`es we applied. Author-defined names are never touched
5719
+ * (they're not in `assigned`). Safe to call more than once (e.g. on both a
5720
+ * finished and an interrupted transition).
5721
+ */
5722
+ function releaseViewTransitionNames(assigned, classed = []) {
5723
+ for (const element of assigned) {
5724
+ element.style?.removeProperty("view-transition-name");
5725
+ }
5726
+ for (const element of classed) {
5727
+ element.style?.removeProperty("view-transition-class");
5728
+ }
5729
+ }
5730
+
5515
5731
  function chooseLayerType(valueName) {
5516
5732
  if (valueName === "layout")
5517
5733
  return "group";
5518
5734
  if (valueName === "enter" || valueName === "new")
5519
5735
  return "new";
5520
- if (valueName === "exit" || valueName === "old")
5521
- return "old";
5522
- return "group";
5736
+ return "old";
5523
5737
  }
5524
5738
 
5525
5739
  let pendingRules = {};
@@ -5554,21 +5768,21 @@
5554
5768
  };
5555
5769
 
5556
5770
  function getViewAnimationLayerInfo(pseudoElement) {
5557
- const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
5771
+ const match = pseudoElement.match(
5772
+ // `group-children` (nested transitions) before `group` so it wins.
5773
+ /::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
5558
5774
  if (!match)
5559
5775
  return null;
5560
5776
  return { layer: match[2], type: match[1] };
5561
5777
  }
5562
5778
 
5563
- function filterViewAnimations(animation) {
5564
- const { effect } = animation;
5565
- if (!effect)
5566
- return false;
5567
- return (effect.target === document.documentElement &&
5568
- effect.pseudoElement?.startsWith("::view-transition"));
5569
- }
5570
5779
  function getViewAnimations() {
5571
- return document.getAnimations().filter(filterViewAnimations);
5780
+ return document.getAnimations().filter((animation) => {
5781
+ const { effect } = animation;
5782
+ return (!!effect &&
5783
+ effect.target === document.documentElement &&
5784
+ effect.pseudoElement?.startsWith("::view-transition"));
5785
+ });
5572
5786
  }
5573
5787
 
5574
5788
  function hasTarget(target, targets) {
@@ -5576,87 +5790,370 @@
5576
5790
  }
5577
5791
 
5578
5792
  const definitionNames = ["layout", "enter", "exit", "new", "old"];
5793
+ /**
5794
+ * The `ViewTransitionTarget` buckets driving each generated layer type, in
5795
+ * priority order - the inverse of `chooseLayerType`. The new view is driven by
5796
+ * `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
5797
+ * bucket; they follow the default layout timing.
5798
+ */
5799
+ const typeBuckets = {
5800
+ group: ["layout"],
5801
+ new: ["new", "enter"],
5802
+ old: ["old", "exit"],
5803
+ };
5804
+ /**
5805
+ * Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
5806
+ * `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
5807
+ * from 1. `enter` prefers the matching `exit` value over these (see below).
5808
+ */
5809
+ const ORIGIN_DEFAULTS = {
5810
+ new: { opacity: 0, scale: 0.85 },
5811
+ old: { opacity: 1, scale: 1 },
5812
+ };
5813
+ const cornerProps = [
5814
+ "borderTopLeftRadius",
5815
+ "borderTopRightRadius",
5816
+ "borderBottomRightRadius",
5817
+ "borderBottomLeftRadius",
5818
+ ];
5579
5819
  function startViewAnimation(builder) {
5580
- const { update, targets, options: defaultOptions } = builder;
5820
+ const { update, targets, resolveDefs, noCrop, pairs, classNames, options: defaultOptions, } = builder;
5581
5821
  if (!document.startViewTransition) {
5582
- return new Promise(async (resolve) => {
5822
+ // An async IIFE (not `new Promise(async )`) so a throwing/rejecting
5823
+ // update rejects this promise rather than leaving it unsettled.
5824
+ return (async () => {
5583
5825
  await update();
5584
- resolve(new GroupAnimation([]));
5826
+ return new GroupAnimation([]);
5827
+ })();
5828
+ }
5829
+ /**
5830
+ * Resolve any selector/Element targets to layer names, assigning a
5831
+ * `view-transition-name` to each element as we go. We run this before the
5832
+ * update (so the elements are captured in the old snapshot) and again
5833
+ * after it (for the new snapshot). An element present in both keeps the
5834
+ * same name and animates as a single `group` layer.
5835
+ */
5836
+ const nameRegistry = new Map();
5837
+ const assigned = [];
5838
+ /**
5839
+ * Elements we tagged with a `view-transition-class` (via `.class()`),
5840
+ * tracked separately from `assigned` so cleanup removes the class without
5841
+ * ever stripping an author's own inline `view-transition-name`.
5842
+ */
5843
+ const classed = [];
5844
+ const layerTargets = new Map();
5845
+ const croppedNames = new Set();
5846
+ /**
5847
+ * Each layer's stagger position (index + total) within its subject, per
5848
+ * snapshot. Resolving against the snapshot the layer belongs to keeps
5849
+ * stagger correct when `update()` replaces the matched elements, and lets
5850
+ * us skip a layer that's absent from a snapshot (e.g. an exited element
5851
+ * has no `new` pseudo-element).
5852
+ */
5853
+ const layerStagger = new Map();
5854
+ /**
5855
+ * Names allocated for a paired subject in the old snapshot, replayed onto
5856
+ * its new-snapshot target so both ends share a layer and morph.
5857
+ */
5858
+ const pairNames = new Map();
5859
+ /**
5860
+ * The old (`from`) elements of each paired subject, so their names can be
5861
+ * transferred off before the new (`to`) elements inherit them.
5862
+ */
5863
+ const pairFrom = new Map();
5864
+ const resolveLayers = (phase) => {
5865
+ targets.forEach((target, definition) => {
5866
+ const className = classNames.get(definition);
5867
+ let names;
5868
+ if (definition === "root" || !resolveDefs.has(definition)) {
5869
+ names = [definition];
5870
+ }
5871
+ else if (pairs.has(definition)) {
5872
+ /**
5873
+ * Paired morph: name the old target in the old snapshot, then
5874
+ * force the same name(s) onto the new target in the new one, so
5875
+ * two different elements morph as a single layer.
5876
+ */
5877
+ if (phase === "old") {
5878
+ pairFrom.set(definition, resolveElements(definition));
5879
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
5880
+ pairNames.set(definition, names);
5881
+ }
5882
+ else {
5883
+ /**
5884
+ * Transfer the name(s) off the `from` elements before the
5885
+ * `to` elements inherit them. A `from` that survives into
5886
+ * the new snapshot (e.g. hidden with `visibility: hidden`
5887
+ * rather than removed) would otherwise keep the name and
5888
+ * collide - "duplicate view-transition-name".
5889
+ */
5890
+ for (const el of pairFrom.get(definition) ?? []) {
5891
+ el.style?.removeProperty("view-transition-name");
5892
+ /**
5893
+ * Drop the old end from the registry too, so the new
5894
+ * end alone supplies this name's `new` crop radii - we
5895
+ * neither re-measure nor get ordered by a stale element.
5896
+ */
5897
+ nameRegistry.delete(el);
5898
+ }
5899
+ names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed);
5900
+ }
5901
+ }
5902
+ else {
5903
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
5904
+ }
5905
+ const cropped = definition !== "root" && !noCrop.has(definition);
5906
+ names.forEach((name, index) => {
5907
+ /**
5908
+ * If two subjects resolve to the same element, merge their
5909
+ * definitions so neither subject's animations are dropped.
5910
+ */
5911
+ const existing = layerTargets.get(name);
5912
+ layerTargets.set(name, existing && existing !== target
5913
+ ? { ...existing, ...target }
5914
+ : target);
5915
+ if (cropped)
5916
+ croppedNames.add(name);
5917
+ const stagger = layerStagger.get(name) ?? {};
5918
+ stagger[phase] = [index, names.length];
5919
+ layerStagger.set(name, stagger);
5920
+ });
5585
5921
  });
5586
- }
5587
- // TODO: Go over existing targets and ensure they all have ids
5922
+ };
5923
+ /**
5924
+ * The stagger index/total for a layer, resolved against the snapshot it
5925
+ * belongs to. Returns index -1 when the layer is absent from that snapshot
5926
+ * so the caller can skip a pseudo-element that doesn't exist.
5927
+ */
5928
+ const staggerPosition = (name, type) => {
5929
+ const stagger = layerStagger.get(name);
5930
+ const position = type === "old"
5931
+ ? stagger?.old
5932
+ : type === "new"
5933
+ ? stagger?.new
5934
+ : // group / group-children / image-pair persist across both.
5935
+ stagger?.new ?? stagger?.old;
5936
+ return position ?? [-1, 1];
5937
+ };
5938
+ /**
5939
+ * Merge default + per-layer transition options for a generated layer and
5940
+ * resolve any stagger/delay function against this element's position. Used
5941
+ * by both the morph-retiming and crop corner-radius passes.
5942
+ */
5943
+ const resolveLayerTransition = (target, type, transitionName, index, total) => {
5944
+ const transition = mergeTransition(getValueTransition$1(defaultOptions, transitionName), getValueTransition$1((layerOptions(target, type) ?? {}), transitionName));
5945
+ if (typeof transition.delay === "function") {
5946
+ transition.delay = transition.delay(index, total);
5947
+ }
5948
+ return transition;
5949
+ };
5950
+ /**
5951
+ * Measured corner radii per cropped layer, so the clip can animate each
5952
+ * corner between the old and new elements. Per-corner (rather than the
5953
+ * shorthand) so mismatched/individual radii interpolate cleanly.
5954
+ */
5955
+ const cropRadii = new Map();
5956
+ const recordRadii = (style, name, phase) => {
5957
+ const corners = {};
5958
+ for (const corner of cornerProps)
5959
+ corners[corner] = style[corner];
5960
+ const entry = cropRadii.get(name) ?? {};
5961
+ entry[phase] = corners;
5962
+ cropRadii.set(name, entry);
5963
+ };
5588
5964
  /**
5589
- * If we don't have any animations defined for the root target,
5590
- * remove it from being captured.
5965
+ * Cropped layers all come from `.add()`, so their elements are in the
5966
+ * registry - read each one's corner radii directly. For a paired morph both
5967
+ * ends share a name; the new-snapshot element is registered last, so it
5968
+ * wins the `new` reading (and the old end the `old` reading).
5591
5969
  */
5592
- if (!hasTarget("root", targets)) {
5593
- css.set(":root", {
5594
- "view-transition-name": "none",
5970
+ const measureCrop = (phase) => {
5971
+ if (!croppedNames.size)
5972
+ return;
5973
+ nameRegistry.forEach((name, element) => {
5974
+ if (croppedNames.has(name)) {
5975
+ recordRadii(getComputedStyle(element), name, phase);
5976
+ }
5595
5977
  });
5596
- }
5978
+ };
5597
5979
  /**
5598
- * Set the timing curve to linear for all view transition layers.
5599
- * This gets baked into the keyframes, which can't be changed
5600
- * without breaking the generated animation.
5980
+ * Write the persistent view-transition CSS: suppress root capture when the
5981
+ * root has no animations of its own; force linear timing (baked into the
5982
+ * keyframes, so we can retime later via updateTiming); and clip +
5983
+ * object-fit: cover every cropped morph (the UA default overflows on
5984
+ * aspect-ratio change), with an animated border-radius added below.
5601
5985
  *
5602
- * This allows us to set easing via updateTiming - which can be changed.
5986
+ * `css.commit` replaces rather than appends, so we re-set the full rule set
5987
+ * each call - the second call (in the update callback) then picks up cropped
5988
+ * layers that only exist in the new snapshot.
5603
5989
  */
5604
- css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
5605
- css.commit(); // Write
5606
- const transition = document.startViewTransition(async () => {
5607
- await update();
5608
- // TODO: Go over new targets and ensure they all have ids
5609
- });
5610
- transition.finished.finally(() => {
5990
+ const commitViewCSS = () => {
5991
+ if (!hasTarget("root", targets)) {
5992
+ css.set(":root", { "view-transition-name": "none" });
5993
+ }
5994
+ css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
5995
+ croppedNames.forEach((name) => {
5996
+ css.set(`::view-transition-group(${name})`, { overflow: "clip" });
5997
+ css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
5998
+ });
5999
+ css.commit(); // Write
6000
+ };
6001
+ const cleanup = () => {
6002
+ releaseViewTransitionNames(assigned, classed);
5611
6003
  css.remove(); // Write
5612
- });
5613
- return new Promise((resolve) => {
5614
- transition.ready.then(() => {
6004
+ };
6005
+ const callback = async () => {
6006
+ await update();
6007
+ /**
6008
+ * Re-resolve so elements created by the update are named for the new
6009
+ * snapshot, then measure the cropped layers' new border-radius.
6010
+ */
6011
+ const croppedBefore = croppedNames.size;
6012
+ resolveLayers("new");
6013
+ measureCrop("new");
6014
+ /**
6015
+ * Re-commit the crop CSS only if the new snapshot introduced cropped
6016
+ * layers, so a layer that exists only in the new snapshot is clipped
6017
+ * too - without forcing a redundant style write on the common path.
6018
+ */
6019
+ if (croppedNames.size > croppedBefore)
6020
+ commitViewCSS();
6021
+ };
6022
+ let transition;
6023
+ try {
6024
+ resolveLayers("old");
6025
+ measureCrop("old");
6026
+ commitViewCSS();
6027
+ transition = document.startViewTransition(callback);
6028
+ }
6029
+ catch (error) {
6030
+ /**
6031
+ * The prelude writes inline names before the transition exists. If it
6032
+ * throws (e.g. startViewTransition rejects in a bad UA state), release
6033
+ * them so we neither leak DOM state nor stall the queue on a promise
6034
+ * that never settles - hand back a rejection it can advance past.
6035
+ */
6036
+ cleanup();
6037
+ return Promise.reject(error);
6038
+ }
6039
+ transition.finished.finally(cleanup);
6040
+ return new Promise((resolve, reject) => {
6041
+ transition.ready
6042
+ .then(() => {
5615
6043
  const generatedViewAnimations = getViewAnimations();
5616
6044
  const animations = [];
5617
6045
  /**
5618
6046
  * Create animations for each of our explicitly-defined subjects.
6047
+ * `opacityAnimated` additionally tracks which `${name}:${type}`
6048
+ * we faded, so we can keep the UA `plus-lighter` blend only for a
6049
+ * genuine opacity crossfade (both sides fading) and drop it for a
6050
+ * slide/transform, where additive compositing would flash bright.
5619
6051
  */
5620
- targets.forEach((definition, target) => {
5621
- // TODO: If target is not "root", resolve elements
5622
- // and iterate over each
6052
+ const explicitlyAnimated = new Set();
6053
+ const opacityAnimated = new Set();
6054
+ layerTargets.forEach((target, name) => {
6055
+ const stagger = layerStagger.get(name);
6056
+ /**
6057
+ * Presence: `enter` only fires for a pure newcomer (a new
6058
+ * view with no old), `exit` only for a pure leaver. A
6059
+ * survivor (both) gets neither - it just morphs.
6060
+ */
6061
+ const enterApplies = !!stagger?.new && !stagger?.old;
6062
+ const exitApplies = !!stagger?.old && !stagger?.new;
5623
6063
  for (const key of definitionNames) {
5624
- if (!definition[key])
6064
+ if (!target[key])
5625
6065
  continue;
5626
- const { keyframes, options } = definition[key];
6066
+ if (key === "enter" && !enterApplies)
6067
+ continue;
6068
+ if (key === "exit" && !exitApplies)
6069
+ continue;
6070
+ const type = chooseLayerType(key);
6071
+ const [index, total] = staggerPosition(name, type);
6072
+ // Skip a layer absent from its snapshot.
6073
+ if (index === -1)
6074
+ continue;
6075
+ const { keyframes, options } = target[key];
5627
6076
  for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
5628
- if (!valueKeyframes)
6077
+ // Skip only missing values - `0` (e.g. opacity: 0)
6078
+ // is valid and must reach the from-value inference.
6079
+ if (valueKeyframes == null)
5629
6080
  continue;
5630
- const valueOptions = {
5631
- ...getValueTransition$1(defaultOptions, valueName),
5632
- ...getValueTransition$1(options, valueName),
5633
- };
5634
- const type = chooseLayerType(key);
5635
6081
  /**
5636
- * If this is an opacity animation, and keyframes are not an array,
5637
- * we need to convert them into an array and set an initial value.
6082
+ * The view path hands keyframes straight to WAAPI,
6083
+ * so Motion's `x`/`y` shorthands (compiled to
6084
+ * `transform` only via the value pipeline) have no
6085
+ * effect. Warn and skip - use `transform`/`translate`.
5638
6086
  */
5639
- if (valueName === "opacity" &&
5640
- !Array.isArray(valueKeyframes)) {
5641
- const initialValue = type === "new" ? 0 : 1;
5642
- valueKeyframes = [initialValue, valueKeyframes];
6087
+ if (valueName === "x" || valueName === "y") {
6088
+ warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
6089
+ continue;
6090
+ }
6091
+ /**
6092
+ * enter/exit win over new/old on a shared property -
6093
+ * skip it here when the gated bucket also defines it.
6094
+ */
6095
+ if (key === "new" &&
6096
+ enterApplies &&
6097
+ target.enter?.keyframes[valueName] != null) {
6098
+ continue;
6099
+ }
6100
+ if (key === "old" &&
6101
+ exitApplies &&
6102
+ target.exit?.keyframes[valueName] != null) {
6103
+ continue;
6104
+ }
6105
+ const valueOptions = mergeTransition(getValueTransition$1(defaultOptions, valueName), getValueTransition$1(options, valueName));
6106
+ /**
6107
+ * Infer an origin for a single-value keyframe. An
6108
+ * `enter` mirrors the matching `exit` value (a
6109
+ * defined exit reverses into the enter for free);
6110
+ * otherwise the per-type default (opacity 0/1, scale
6111
+ * 0.85). No default -> left as-is (animates from the
6112
+ * live value).
6113
+ *
6114
+ * `new`/`old` fire for survivors too, where only the
6115
+ * opacity crossfade default applies - a transform
6116
+ * default like scale 0.85 would pop a persisting
6117
+ * element, so gate it on the layer actually
6118
+ * entering/leaving.
6119
+ */
6120
+ if (!Array.isArray(valueKeyframes)) {
6121
+ const exitValue = key === "enter"
6122
+ ? target.exit?.keyframes[valueName]
6123
+ : undefined;
6124
+ const allowDefault = valueName === "opacity" ||
6125
+ (type === "new" ? enterApplies : exitApplies);
6126
+ const from = exitValue != null
6127
+ ? Array.isArray(exitValue)
6128
+ ? exitValue[exitValue.length - 1]
6129
+ : exitValue
6130
+ : allowDefault
6131
+ ? ORIGIN_DEFAULTS[type]?.[valueName]
6132
+ : undefined;
6133
+ if (from !== undefined) {
6134
+ valueKeyframes = [from, valueKeyframes];
6135
+ }
5643
6136
  }
5644
6137
  /**
5645
- * Resolve stagger function if provided.
6138
+ * Resolve stagger function if provided, per element
6139
+ * across this subject's resolved layers.
5646
6140
  */
5647
6141
  if (typeof valueOptions.delay === "function") {
5648
- valueOptions.delay = valueOptions.delay(0, 1);
6142
+ valueOptions.delay = valueOptions.delay(index, total);
5649
6143
  }
5650
6144
  valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
5651
6145
  valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
5652
- const animation = new NativeAnimation({
6146
+ animations.push(new NativeAnimation({
5653
6147
  ...valueOptions,
5654
6148
  element: document.documentElement,
5655
6149
  name: valueName,
5656
- pseudoElement: `::view-transition-${type}(${target})`,
6150
+ pseudoElement: `::view-transition-${type}(${name})`,
5657
6151
  keyframes: valueKeyframes,
5658
- });
5659
- animations.push(animation);
6152
+ }));
6153
+ explicitlyAnimated.add(`${name}:${type}`);
6154
+ if (valueName === "opacity") {
6155
+ opacityAnimated.add(`${name}:${type}`);
6156
+ }
5660
6157
  }
5661
6158
  }
5662
6159
  });
@@ -5675,45 +6172,161 @@
5675
6172
  const name = getViewAnimationLayerInfo(pseudoElement);
5676
6173
  if (!name)
5677
6174
  continue;
5678
- const targetDefinition = targets.get(name.layer);
5679
- if (!targetDefinition) {
5680
- /**
5681
- * If transition name is group then update the timing of the animation
5682
- * whereas if it's old or new then we could possibly replace it using
5683
- * the above method.
5684
- */
5685
- const transitionName = name.type === "group" ? "layout" : "";
5686
- let animationTransition = {
5687
- ...getValueTransition$1(defaultOptions, transitionName),
5688
- };
5689
- animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
5690
- animationTransition =
5691
- applyGeneratorOptions(animationTransition);
5692
- const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
5693
- effect.updateTiming({
5694
- delay: secondsToMilliseconds(animationTransition.delay ?? 0),
5695
- duration: animationTransition.duration,
5696
- easing,
5697
- });
5698
- animations.push(new NativeAnimationWrapper(animation));
5699
- }
5700
- else if (hasOpacity(targetDefinition, "enter") &&
5701
- hasOpacity(targetDefinition, "exit") &&
5702
- effect
5703
- .getKeyframes()
5704
- .some((keyframe) => keyframe.mixBlendMode)) {
5705
- animations.push(new NativeAnimationWrapper(animation));
5706
- }
5707
- else {
5708
- animation.cancel();
6175
+ const targetDefinition = layerTargets.get(name.layer);
6176
+ /**
6177
+ * We built our own animation for this layer, so drop the
6178
+ * browser-generated fade we're replacing. The UA
6179
+ * `plus-lighter` blend is a *separate* generated animation on
6180
+ * the same pseudo (it sets `mix-blend-mode` in its keyframes):
6181
+ * keep it *only* for a true opacity crossfade - both sides
6182
+ * fading - so a symmetric crossfade composites without
6183
+ * darkening, but a slide/transform (where both layers stay
6184
+ * opaque and overlap) doesn't flash bright from the addition.
6185
+ */
6186
+ if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
6187
+ const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
6188
+ opacityAnimated.has(`${name.layer}:old`);
6189
+ if (isCrossfade &&
6190
+ effect
6191
+ .getKeyframes()
6192
+ .some((keyframe) => keyframe.mixBlendMode)) {
6193
+ animations.push(new NativeAnimationWrapper(animation));
6194
+ }
6195
+ else {
6196
+ animation.cancel();
6197
+ }
6198
+ continue;
5709
6199
  }
6200
+ /**
6201
+ * Otherwise retime the browser-generated animation to
6202
+ * Motion's timing. This auto-enables the layout (group)
6203
+ * morph for any resolved/named target, and applies the
6204
+ * default timing to old/new layers we haven't explicitly
6205
+ * overridden.
6206
+ *
6207
+ * group + group-children both follow the layout timing so
6208
+ * the nesting container stays in sync with the morph.
6209
+ */
6210
+ /**
6211
+ * A survivor's old + new are the two halves of one
6212
+ * `plus-lighter` crossfade. They must share identical timing
6213
+ * (so their opacities stay mirrored and sum to 1 - else the
6214
+ * additive blend flashes bright wherever both are partly
6215
+ * visible) and fade linearly (the bounce belongs on the
6216
+ * group's geometry, not the opacity). So time them as the
6217
+ * group, rather than via their own - potentially staggered,
6218
+ * or enter/exit-derived - old/new options.
6219
+ */
6220
+ const stagger = layerStagger.get(name.layer);
6221
+ const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
6222
+ !!stagger?.old &&
6223
+ !!stagger?.new;
6224
+ const timingType = name.type.startsWith("group") || isMorphCrossfade
6225
+ ? "group"
6226
+ : name.type;
6227
+ const [index, total] = staggerPosition(name.layer, timingType);
6228
+ const transitionName = timingType === "group" ? "layout" : "";
6229
+ let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
6230
+ /**
6231
+ * The crossfade should resolve at the spring's *perceptual*
6232
+ * (visual) duration - the geometry can keep bouncing, but the
6233
+ * opacity shouldn't drag through the settle. So capture
6234
+ * `visualDuration` before `applyGeneratorOptions` replaces it
6235
+ * with the full overshoot duration, and use it for the fade.
6236
+ */
6237
+ const visualDuration = animationTransition.visualDuration;
6238
+ animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
6239
+ animationTransition =
6240
+ applyGeneratorOptions(animationTransition);
6241
+ const duration = isMorphCrossfade && visualDuration !== undefined
6242
+ ? secondsToMilliseconds(visualDuration)
6243
+ : animationTransition.duration;
6244
+ const easing = isMorphCrossfade
6245
+ ? "linear"
6246
+ : mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
6247
+ effect.updateTiming({
6248
+ delay: secondsToMilliseconds(animationTransition.delay ?? 0),
6249
+ duration,
6250
+ easing,
6251
+ });
6252
+ animations.push(new NativeAnimationWrapper(animation));
5710
6253
  }
6254
+ /**
6255
+ * Animate each cropped layer's clip corners between the old and
6256
+ * new elements, so a cropped morph keeps rounded corners
6257
+ * (handling individual per-corner radii).
6258
+ */
6259
+ cropRadii.forEach((radii, name) => {
6260
+ if (!radii.old && !radii.new)
6261
+ return;
6262
+ const target = layerTargets.get(name);
6263
+ const [index, total] = staggerPosition(name, "group");
6264
+ const radiusOptions = resolveLayerTransition(target, "group", "layout", index === -1 ? 0 : index, total);
6265
+ radiusOptions.duration && (radiusOptions.duration = secondsToMilliseconds(radiusOptions.duration));
6266
+ radiusOptions.delay && (radiusOptions.delay = secondsToMilliseconds(radiusOptions.delay));
6267
+ for (const corner of cornerProps) {
6268
+ // `||` (not `??`) so an empty measurement (e.g. an
6269
+ // un-rendered element) falls back rather than producing
6270
+ // an invalid keyframe.
6271
+ const from = radii.old?.[corner] || radii.new?.[corner] || "0px";
6272
+ const to = radii.new?.[corner] || radii.old?.[corner] || "0px";
6273
+ // Skip square corners - nothing to round.
6274
+ if (parseFloat(from) === 0 && parseFloat(to) === 0) {
6275
+ continue;
6276
+ }
6277
+ animations.push(new NativeAnimation({
6278
+ ...radiusOptions,
6279
+ element: document.documentElement,
6280
+ name: corner,
6281
+ pseudoElement: `::view-transition-group(${name})`,
6282
+ keyframes: [from, to],
6283
+ }));
6284
+ }
6285
+ });
5711
6286
  resolve(new GroupAnimation(animations));
5712
- });
6287
+ })
6288
+ .catch(() =>
6289
+ /**
6290
+ * `ready` rejects when the transition is skipped - no visual
6291
+ * change, or superseded by an interrupting transition. The DOM
6292
+ * update still applied, so settle with no animations rather than
6293
+ * surfacing it as an error to an awaiting caller. A genuine
6294
+ * failure in `update()` rejects `updateCallbackDone` (already
6295
+ * settled by now), so propagate that instead.
6296
+ */
6297
+ transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
5713
6298
  });
5714
6299
  }
5715
- function hasOpacity(target, key) {
5716
- return target?.[key]?.keyframes.opacity;
6300
+ /**
6301
+ * The options that should time a given generated layer type, so a retimed
6302
+ * group/old/new picks up any per-target transition the user provided. Checks
6303
+ * the type's buckets in priority order (e.g. `new` before `enter`).
6304
+ */
6305
+ function layerOptions(target, type) {
6306
+ for (const bucket of typeBuckets[type] ?? []) {
6307
+ const options = target?.[bucket]?.options;
6308
+ if (options)
6309
+ return options;
6310
+ }
6311
+ }
6312
+ /**
6313
+ * Merge a base transition (e.g. the default `options`) with a per-layer/value
6314
+ * override. An explicit `duration` on the override must win over an inherited
6315
+ * generator's own timing: a spring prefers `visualDuration`, and
6316
+ * `spring.applyToOptions` overwrites `duration` with the computed settle time -
6317
+ * so without this the override is silently discarded. Dropping the inherited
6318
+ * `type`/`visualDuration` makes the layer a plain tween of that duration, unless
6319
+ * it asked for its own generator `type`/`visualDuration`.
6320
+ */
6321
+ function mergeTransition(base, override) {
6322
+ const merged = { ...base, ...override };
6323
+ if (override.duration !== undefined) {
6324
+ if (override.visualDuration === undefined)
6325
+ delete merged.visualDuration;
6326
+ if (override.type === undefined)
6327
+ delete merged.type;
6328
+ }
6329
+ return merged;
5717
6330
  }
5718
6331
 
5719
6332
  let builders = [];
@@ -5727,10 +6340,16 @@
5727
6340
  function start(builder) {
5728
6341
  removeItem(builders, builder);
5729
6342
  current = builder;
5730
- startViewAnimation(builder).then((animation) => {
6343
+ startViewAnimation(builder)
6344
+ .then((animation) => {
5731
6345
  builder.notifyReady(animation);
5732
- animation.finished.finally(next);
5733
- });
6346
+ return animation.finished;
6347
+ })
6348
+ // A genuinely failed transition (a throwing update) rejects the
6349
+ // builder; a skipped/interrupted one resolves with no animations (see
6350
+ // start.ts). Either way, advance the queue - else later transitions hang.
6351
+ .catch((error) => builder.notifyReject(error))
6352
+ .finally(next);
5734
6353
  }
5735
6354
  function processQueue() {
5736
6355
  /**
@@ -5767,31 +6386,96 @@
5767
6386
  constructor(update, options = {}) {
5768
6387
  this.currentSubject = "root";
5769
6388
  this.targets = new Map();
5770
- this.notifyReady = noop$1;
5771
- this.readyPromise = new Promise((resolve) => {
6389
+ /**
6390
+ * Definitions that must be resolved to elements (and assigned a
6391
+ * `view-transition-name`) rather than treated as pre-named layers.
6392
+ */
6393
+ this.resolveDefs = new Set();
6394
+ /**
6395
+ * Subjects opted out of the default crop (clip + object-fit: cover +
6396
+ * animated corner radii) via `.crop(false)`.
6397
+ */
6398
+ this.noCrop = new Set();
6399
+ /**
6400
+ * Subjects paired with a different new-snapshot target (the second `.add()`
6401
+ * argument), so two distinct elements share one name and morph into each
6402
+ * other - a shared-element transition.
6403
+ */
6404
+ this.pairs = new Map();
6405
+ /**
6406
+ * A `view-transition-class` to apply to each subject's resolved elements,
6407
+ * so authors can target the generated layers from CSS by class rather than
6408
+ * the opaque generated name.
6409
+ */
6410
+ this.classNames = new Map();
6411
+ this.notifyReady = noop;
6412
+ this.notifyReject = noop;
6413
+ this.readyPromise = new Promise((resolve, reject) => {
5772
6414
  this.notifyReady = resolve;
6415
+ this.notifyReject = reject;
5773
6416
  });
5774
6417
  this.update = update;
5775
6418
  this.options = {
5776
6419
  interrupt: "wait",
5777
6420
  ...options,
5778
6421
  };
6422
+ // Avoid an unhandled rejection when a failed transition has no
6423
+ // `.then(_, reject)` handler attached (e.g. fire-and-forget).
6424
+ this.readyPromise.catch(noop);
5779
6425
  addToQueue(this);
5780
6426
  }
5781
- get(subject) {
6427
+ /**
6428
+ * Target elements resolved from a selector or Element, each assigned a
6429
+ * `view-transition-name` automatically.
6430
+ *
6431
+ * Passing a second target pairs them: the first is resolved in the old
6432
+ * snapshot and the second in the new, sharing one name so two *different*
6433
+ * elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
6434
+ * pass them the other way round to morph back.
6435
+ */
6436
+ add(subject, newSubject) {
5782
6437
  this.currentSubject = subject;
6438
+ this.resolveDefs.add(subject);
6439
+ if (newSubject !== undefined)
6440
+ this.pairs.set(subject, newSubject);
6441
+ // Register the subject so it participates (and gets an automatic
6442
+ // layout/morph animation) even without an explicit enter/exit/layout.
6443
+ if (!this.targets.has(subject))
6444
+ this.targets.set(subject, {});
5783
6445
  return this;
5784
6446
  }
5785
- layout(keyframes, options) {
5786
- this.updateTarget("layout", keyframes, options);
6447
+ /**
6448
+ * Morphs are clipped + `object-fit: cover` (and their corners animate)
6449
+ * by default. Call `.crop(false)` to opt this subject out and fall back
6450
+ * to the browser default (which overflows on aspect-ratio change).
6451
+ */
6452
+ crop(enabled = true) {
6453
+ enabled
6454
+ ? this.noCrop.delete(this.currentSubject)
6455
+ : this.noCrop.add(this.currentSubject);
5787
6456
  return this;
5788
6457
  }
5789
- new(keyframes, options) {
5790
- this.updateTarget("new", keyframes, options);
6458
+ /**
6459
+ * Tag this subject's generated layers with a `view-transition-class`, so
6460
+ * they can be targeted from CSS - `::view-transition-group(.name)`,
6461
+ * `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
6462
+ * - without the opaque generated `view-transition-name`. Because `.add()`
6463
+ * can match many elements, a shared class targets them all at once (and,
6464
+ * for a pair, both ends). The escape hatch for z-index / custom keyframes
6465
+ * on a morph layer.
6466
+ */
6467
+ class(name) {
6468
+ this.classNames.set(this.currentSubject, name);
5791
6469
  return this;
5792
6470
  }
5793
- old(keyframes, options) {
5794
- this.updateTarget("old", keyframes, options);
6471
+ /**
6472
+ * Set the transition for this subject's morph. The morph is enabled
6473
+ * automatically by `.add()`; this just customises its timing (duration,
6474
+ * easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
6475
+ * opts the page into the transition (the root crossfade).
6476
+ */
6477
+ layout(options = {}) {
6478
+ this.updateTarget("layout", {}, options);
5795
6479
  return this;
5796
6480
  }
5797
6481
  enter(keyframes, options) {
@@ -5802,9 +6486,21 @@
5802
6486
  this.updateTarget("exit", keyframes, options);
5803
6487
  return this;
5804
6488
  }
5805
- crossfade(options) {
5806
- this.updateTarget("enter", { opacity: 1 }, options);
5807
- this.updateTarget("exit", { opacity: 0 }, options);
6489
+ /**
6490
+ * Animate the new view directly, whether the element is appearing or
6491
+ * persisting (unlike `.enter()`, which only fires for a pure newcomer).
6492
+ * Pair with `.old()` for a crossfade or slide-through.
6493
+ */
6494
+ new(keyframes, options) {
6495
+ this.updateTarget("new", keyframes, options);
6496
+ return this;
6497
+ }
6498
+ /**
6499
+ * Animate the old view directly, whether the element is leaving or
6500
+ * persisting (unlike `.exit()`, which only fires for a pure leaver).
6501
+ */
6502
+ old(keyframes, options) {
6503
+ this.updateTarget("old", keyframes, options);
5808
6504
  return this;
5809
6505
  }
5810
6506
  updateTarget(target, keyframes, options = {}) {
@@ -5819,8 +6515,8 @@
5819
6515
  return this.readyPromise.then(resolve, reject);
5820
6516
  }
5821
6517
  }
5822
- function animateView(update, defaultOptions = {}) {
5823
- return new ViewTransitionBuilder(update, defaultOptions);
6518
+ function animateView(update, options = {}) {
6519
+ return new ViewTransitionBuilder(update, options);
5824
6520
  }
5825
6521
 
5826
6522
  const createAxisDelta = () => ({
@@ -6767,6 +7463,14 @@
6767
7463
  }
6768
7464
  }
6769
7465
  }
7466
+ // `pathRotation` composes onto `rotate` as a separate additive term so
7467
+ // the user's `rotate` is never clobbered. Deliberately not a slot in
7468
+ // `transformPropOrder`.
7469
+ const pathRotation = latestValues.pathRotation;
7470
+ if (pathRotation) {
7471
+ transformIsDefault = false;
7472
+ transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
7473
+ }
6770
7474
  transformString = transformString.trim();
6771
7475
  // If we have a custom `transform` template, pass our transform values and
6772
7476
  // generated transformString to that before returning
@@ -7829,11 +8533,14 @@
7829
8533
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
7830
8534
  }
7831
8535
  if (latestTransform) {
7832
- const { transformPerspective, rotate, rotateX, rotateY, skewX, skewY } = latestTransform;
8536
+ const { transformPerspective, rotate, pathRotation, rotateX, rotateY, skewX, skewY, } = latestTransform;
7833
8537
  if (transformPerspective)
7834
8538
  transform = `perspective(${transformPerspective}px) ${transform}`;
7835
8539
  if (rotate)
7836
8540
  transform += `rotate(${rotate}deg) `;
8541
+ // Additive `rotate()` so user `rotate` isn't clobbered.
8542
+ if (pathRotation)
8543
+ transform += `rotate(${pathRotation}deg) `;
7837
8544
  if (rotateX)
7838
8545
  transform += `rotateX(${rotateX}deg) `;
7839
8546
  if (rotateY)
@@ -7909,7 +8616,7 @@
7909
8616
  : values.borderRadius;
7910
8617
  }
7911
8618
  const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
7912
- const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop$1);
8619
+ const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
7913
8620
  function compress(min, max, easing) {
7914
8621
  return (p) => {
7915
8622
  // Could replace ifs with clamp
@@ -7929,7 +8636,7 @@
7929
8636
 
7930
8637
  function addDomEvent(target, eventName, handler, options = { passive: true }) {
7931
8638
  target.addEventListener(eventName, handler, options);
7932
- return () => target.removeEventListener(eventName, handler);
8639
+ return () => target.removeEventListener(eventName, handler, options);
7933
8640
  }
7934
8641
 
7935
8642
  const compareByDepth = (a, b) => a.depth - b.depth;
@@ -8388,7 +9095,8 @@
8388
9095
  * Set animation origin after starting animation to avoid layout jump
8389
9096
  * caused by stopping previous layout animation
8390
9097
  */
8391
- this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
9098
+ this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged, animationOptions
9099
+ .path);
8392
9100
  }
8393
9101
  else {
8394
9102
  /**
@@ -9104,7 +9812,7 @@
9104
9812
  this.projectionDelta = createDelta();
9105
9813
  this.projectionDeltaWithTransform = createDelta();
9106
9814
  }
9107
- setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
9815
+ setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false, pathFn) {
9108
9816
  const snapshot = this.snapshot;
9109
9817
  const snapshotLatestValues = snapshot ? snapshot.latestValues : {};
9110
9818
  const mixedValues = { ...this.latestValues };
@@ -9126,10 +9834,26 @@
9126
9834
  !this.path.some(hasOpacityCrossfade));
9127
9835
  this.animationProgress = 0;
9128
9836
  let prevRelativeTarget;
9837
+ // The path decides whether the layout shift is worth curving
9838
+ // (distance floor) and resolves the interpolator from the delta.
9839
+ const interpolate = pathFn?.interpolateProjection(delta);
9129
9840
  this.mixTargetDelta = (latest) => {
9130
9841
  const progress = latest / 1000;
9131
- mixAxisDelta(targetDelta.x, delta.x, progress);
9132
- mixAxisDelta(targetDelta.y, delta.y, progress);
9842
+ const point = interpolate?.(progress);
9843
+ if (point) {
9844
+ targetDelta.x.translate = point.x;
9845
+ targetDelta.x.scale = mixNumber$1(delta.x.scale, 1, progress);
9846
+ targetDelta.x.origin = delta.x.origin;
9847
+ targetDelta.x.originPoint = delta.x.originPoint;
9848
+ targetDelta.y.translate = point.y;
9849
+ targetDelta.y.scale = mixNumber$1(delta.y.scale, 1, progress);
9850
+ targetDelta.y.origin = delta.y.origin;
9851
+ targetDelta.y.originPoint = delta.y.originPoint;
9852
+ }
9853
+ else {
9854
+ mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
9855
+ mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
9856
+ }
9133
9857
  this.setTargetDelta(targetDelta);
9134
9858
  if (this.relativeTarget &&
9135
9859
  this.relativeTargetOrigin &&
@@ -9154,6 +9878,13 @@
9154
9878
  this.animationValues = mixedValues;
9155
9879
  mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
9156
9880
  }
9881
+ if (point && point.rotate !== undefined) {
9882
+ // Dedicated `pathRotation` channel, not `rotate`, so an
9883
+ // animating `rotate` is composed with, never clobbered.
9884
+ if (!this.animationValues)
9885
+ this.animationValues = mixedValues;
9886
+ this.animationValues.pathRotation = point.rotate;
9887
+ }
9157
9888
  this.root.scheduleUpdateProjection();
9158
9889
  this.scheduleRender();
9159
9890
  this.animationProgress = progress;
@@ -9175,7 +9906,6 @@
9175
9906
  */
9176
9907
  this.pendingAnimation = frame.update(() => {
9177
9908
  globalProjectionState.hasAnimatedSinceResize = true;
9178
- activeAnimations.layout++;
9179
9909
  this.motionValue || (this.motionValue = motionValue(0));
9180
9910
  this.motionValue.jump(0, false);
9181
9911
  this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
@@ -9186,11 +9916,7 @@
9186
9916
  this.mixTargetDelta(latest);
9187
9917
  options.onUpdate && options.onUpdate(latest);
9188
9918
  },
9189
- onStop: () => {
9190
- activeAnimations.layout--;
9191
- },
9192
9919
  onComplete: () => {
9193
- activeAnimations.layout--;
9194
9920
  options.onComplete && options.onComplete();
9195
9921
  this.completeAnimation();
9196
9922
  },
@@ -9674,7 +10400,7 @@
9674
10400
  function removeLeadSnapshots(stack) {
9675
10401
  stack.removeLeadSnapshot();
9676
10402
  }
9677
- function mixAxisDelta(output, delta, p) {
10403
+ function mixAxisDeltaLinear(output, delta, p) {
9678
10404
  output.translate = mixNumber$1(delta.translate, 0, p);
9679
10405
  output.scale = mixNumber$1(delta.scale, 1, p);
9680
10406
  output.origin = delta.origin;
@@ -9705,7 +10431,7 @@
9705
10431
  */
9706
10432
  const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
9707
10433
  ? Math.round
9708
- : noop$1;
10434
+ : noop;
9709
10435
  function roundAxis(axis) {
9710
10436
  // Round to the nearest .5 pixels to support subpixel layouts
9711
10437
  axis.min = roundPoint(axis.min);
@@ -9779,39 +10505,221 @@
9779
10505
  checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9780
10506
  });
9781
10507
 
9782
- const layoutSelector = "[data-layout], [data-layout-id]";
9783
- const noop = () => { };
9784
- function snapshotFromTarget(projection) {
9785
- const target = projection.targetWithTransforms || projection.target;
9786
- if (!target)
9787
- return undefined;
9788
- const measuredBox = createBox();
9789
- const layoutBox = createBox();
9790
- copyBoxInto(measuredBox, target);
9791
- copyBoxInto(layoutBox, target);
10508
+ const layoutSelector = "[data-layout],[data-layout-id]";
10509
+ /**
10510
+ * All imperatively-created projection nodes live in one persistent tree,
10511
+ * shared across animateLayout() calls (and with any React-created nodes,
10512
+ * via the singleton document root). Keyed by element for reuse.
10513
+ */
10514
+ const layoutNodes = new WeakMap();
10515
+ /**
10516
+ * Builders created within the same synchronous tick are flushed together
10517
+ * as a single "commit": every node is snapshotted before any updateDom
10518
+ * runs, mirroring React batching renders from different parts of the tree.
10519
+ */
10520
+ let pendingBuilders;
10521
+ function collectLayoutElements(scope) {
10522
+ const elements = [];
10523
+ if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
10524
+ elements.push(scope);
10525
+ }
10526
+ scope.querySelectorAll(layoutSelector).forEach((element) => {
10527
+ if (element instanceof HTMLElement)
10528
+ elements.push(element);
10529
+ });
10530
+ return elements;
10531
+ }
10532
+ /**
10533
+ * Process any work scheduled on the frameloop now. A previous animation
10534
+ * may have been seeked while paused (controls.time = x) without a frame
10535
+ * having rendered it - we must materialise that state into the DOM
10536
+ * before taking snapshots.
10537
+ */
10538
+ function flushPendingFrame() {
10539
+ if (frameData.isProcessing)
10540
+ return;
10541
+ const now = time.now();
10542
+ frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
10543
+ frameData.timestamp = now;
10544
+ frameData.isProcessing = true;
10545
+ frameSteps.update.process(frameData);
10546
+ frameSteps.preRender.process(frameData);
10547
+ frameSteps.render.process(frameData);
10548
+ frameData.isProcessing = false;
10549
+ }
10550
+ function getProjectionParent(element) {
10551
+ let ancestor = element.parentElement;
10552
+ while (ancestor) {
10553
+ const node = layoutNodes.get(ancestor);
10554
+ if (node && node.instance)
10555
+ return node;
10556
+ ancestor = ancestor.parentElement;
10557
+ }
10558
+ return undefined;
10559
+ }
10560
+ function createVisualElement() {
10561
+ return new HTMLVisualElement({
10562
+ props: {},
10563
+ presenceContext: null,
10564
+ visualState: {
10565
+ latestValues: {},
10566
+ renderState: {
10567
+ transform: {},
10568
+ transformOrigin: {},
10569
+ style: {},
10570
+ vars: {},
10571
+ },
10572
+ },
10573
+ }, { allowProjection: true });
10574
+ }
10575
+ function readNodeOptions(element, transition) {
10576
+ const layoutAttr = element.getAttribute("data-layout");
10577
+ const layoutId = element.getAttribute("data-layout-id") ?? undefined;
9792
10578
  return {
9793
- animationId: projection.root?.animationId ?? 0,
9794
- measuredBox,
9795
- layoutBox,
9796
- latestValues: projection.animationValues || projection.latestValues || {},
9797
- source: projection.id,
10579
+ layoutId,
10580
+ layout: layoutAttr !== null ? true : undefined,
10581
+ animationType: (!layoutAttr || layoutAttr === "true"
10582
+ ? "both"
10583
+ : layoutAttr),
10584
+ transition,
10585
+ };
10586
+ }
10587
+ function prepareNode(element, transition) {
10588
+ let node = layoutNodes.get(element);
10589
+ if (!node) {
10590
+ let visualElement = visualElementStore.get(element);
10591
+ if (!visualElement)
10592
+ visualElement = createVisualElement();
10593
+ /**
10594
+ * A first-time element may carry a projection transform in its
10595
+ * inline style (e.g. it was cloned from an element mid-animation).
10596
+ * That transform isn't tracked in latestValues so the engine can't
10597
+ * reset it before measuring - clear it now so the first layout
10598
+ * measurement isn't inflated.
10599
+ */
10600
+ if (element.style.transform &&
10601
+ !hasTransform(visualElement.latestValues)) {
10602
+ element.style.transform = "";
10603
+ }
10604
+ node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
10605
+ visualElement.projection = node;
10606
+ node.setOptions({
10607
+ ...readNodeOptions(element, transition),
10608
+ visualElement,
10609
+ });
10610
+ node.mount(element);
10611
+ layoutNodes.set(element, node);
10612
+ }
10613
+ else {
10614
+ node.setOptions(readNodeOptions(element, transition));
10615
+ }
10616
+ node.isPresent = true;
10617
+ if (node.options.onExitComplete) {
10618
+ node.setOptions({ onExitComplete: undefined });
10619
+ }
10620
+ return node;
10621
+ }
10622
+ function sortDocumentOrder(elements) {
10623
+ return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
10624
+ }
10625
+ function dropNode(element, node) {
10626
+ node.setOptions({ onExitComplete: undefined });
10627
+ /**
10628
+ * Stop any lingering animation so it can't leak into future updates.
10629
+ * A follow node can share its currentAnimation with a surviving lead
10630
+ * (via resumingFrom), in which case it isn't ours to stop.
10631
+ */
10632
+ const stack = node.getStack();
10633
+ if (!stack || node.isLead())
10634
+ node.currentAnimation?.stop();
10635
+ node.unmount();
10636
+ layoutNodes.delete(element);
10637
+ }
10638
+ function flushPendingBuilders() {
10639
+ const builders = pendingBuilders;
10640
+ pendingBuilders = undefined;
10641
+ flushPendingFrame();
10642
+ /**
10643
+ * Discover and mount every node across all builders before snapshotting
10644
+ * any of them. Mounting during an active update flags isLayoutDirty,
10645
+ * which would make that node's own willUpdate skip its snapshot.
10646
+ * Document order guarantees ancestors mount before descendants, even
10647
+ * when they're discovered by different builders.
10648
+ */
10649
+ const targets = new Map();
10650
+ for (const builder of builders) {
10651
+ for (const element of builder.collectTargets()) {
10652
+ const owners = targets.get(element);
10653
+ owners ? owners.push(builder) : targets.set(element, [builder]);
10654
+ }
10655
+ }
10656
+ const union = new Map();
10657
+ for (const element of sortDocumentOrder(targets.keys())) {
10658
+ const owners = targets.get(element);
10659
+ const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
10660
+ for (const owner of owners)
10661
+ owner.adopt(element, node);
10662
+ union.set(element, node);
10663
+ }
10664
+ union.forEach((node) => {
10665
+ node.isLayoutDirty = false;
10666
+ node.willUpdate();
10667
+ });
10668
+ const updatePromises = [];
10669
+ for (const builder of builders) {
10670
+ const result = builder.runUpdate();
10671
+ if (result)
10672
+ updatePromises.push(result);
10673
+ }
10674
+ const commit = () => {
10675
+ /**
10676
+ * Process all additions before any removals so that, even across
10677
+ * builders, a removed member knows whether a replacement with the
10678
+ * same layoutId was added in this commit.
10679
+ */
10680
+ const newMemberIds = new Set();
10681
+ for (const builder of builders) {
10682
+ builder.reconcileAdditions(newMemberIds);
10683
+ }
10684
+ for (const builder of builders) {
10685
+ builder.reconcileRemovals(newMemberIds);
10686
+ }
10687
+ let root;
10688
+ union.forEach((node) => (root || (root = node.root)));
10689
+ for (const builder of builders)
10690
+ root || (root = builder.getRoot());
10691
+ root?.didUpdate();
10692
+ /**
10693
+ * The root flushes the update on a microtask, synchronously
10694
+ * processing the frame that creates the layout animations. Collect
10695
+ * them in a later microtask step of the same pass.
10696
+ */
10697
+ microtask.render(() => {
10698
+ for (const builder of builders)
10699
+ builder.finalize();
10700
+ });
9798
10701
  };
10702
+ updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
9799
10703
  }
9800
10704
  class LayoutAnimationBuilder {
9801
10705
  constructor(scope, updateDom, defaultOptions) {
9802
- this.sharedTransitions = new Map();
9803
- this.notifyReady = noop;
9804
- this.rejectReady = noop;
9805
10706
  this.scope = scope;
9806
10707
  this.updateDom = updateDom;
9807
10708
  this.defaultOptions = defaultOptions;
10709
+ this.sharedTransitions = new Map();
10710
+ this.notifyReady = () => { };
10711
+ this.rejectReady = () => { };
10712
+ this.tracked = new Map();
10713
+ this.restorePoints = new Map();
9808
10714
  this.readyPromise = new Promise((resolve, reject) => {
9809
10715
  this.notifyReady = resolve;
9810
10716
  this.rejectReady = reject;
9811
10717
  });
9812
- frame.postRender(() => {
9813
- this.start().then(this.notifyReady).catch(this.rejectReady);
9814
- });
10718
+ if (!pendingBuilders) {
10719
+ pendingBuilders = [];
10720
+ queueMicrotask(flushPendingBuilders);
10721
+ }
10722
+ pendingBuilders.push(this);
9815
10723
  }
9816
10724
  shared(id, transition) {
9817
10725
  this.sharedTransitions.set(id, transition);
@@ -9820,114 +10728,107 @@
9820
10728
  then(resolve, reject) {
9821
10729
  return this.readyPromise.then(resolve, reject);
9822
10730
  }
9823
- async start() {
9824
- const beforeElements = collectLayoutElements(this.scope);
9825
- const beforeRecords = this.buildRecords(beforeElements);
9826
- beforeRecords.forEach(({ projection }) => {
9827
- const hasCurrentAnimation = Boolean(projection.currentAnimation);
9828
- const isSharedLayout = Boolean(projection.options.layoutId);
9829
- if (hasCurrentAnimation && isSharedLayout) {
9830
- const snapshot = snapshotFromTarget(projection);
9831
- if (snapshot) {
9832
- projection.snapshot = snapshot;
9833
- }
9834
- else if (projection.snapshot) {
9835
- projection.snapshot = undefined;
9836
- }
9837
- }
9838
- else if (projection.snapshot &&
9839
- (projection.currentAnimation || projection.isProjecting())) {
9840
- projection.snapshot = undefined;
9841
- }
9842
- projection.isPresent = true;
9843
- projection.willUpdate();
10731
+ transitionFor(element) {
10732
+ const layoutId = element.getAttribute("data-layout-id");
10733
+ return ((layoutId && this.sharedTransitions.get(layoutId)) ||
10734
+ this.defaultOptions);
10735
+ }
10736
+ adopt(element, node) {
10737
+ this.tracked.set(element, node);
10738
+ this.restorePoints.set(element, {
10739
+ parent: element.parentElement,
10740
+ next: element.nextSibling,
9844
10741
  });
9845
- await this.updateDom();
9846
- const afterElements = collectLayoutElements(this.scope);
9847
- const afterRecords = this.buildRecords(afterElements);
9848
- this.handleExitingElements(beforeRecords, afterRecords);
9849
- afterRecords.forEach(({ projection }) => {
9850
- const instance = projection.instance;
9851
- const resumeFromInstance = projection.resumeFrom
9852
- ?.instance;
9853
- if (!instance || !resumeFromInstance)
9854
- return;
9855
- if (!("style" in instance))
9856
- return;
9857
- const currentTransform = instance.style.transform;
9858
- const resumeFromTransform = resumeFromInstance.style.transform;
9859
- if (currentTransform &&
9860
- resumeFromTransform &&
9861
- currentTransform === resumeFromTransform) {
9862
- instance.style.transform = "";
9863
- instance.style.transformOrigin = "";
10742
+ }
10743
+ collectTargets() {
10744
+ return collectLayoutElements(this.scope);
10745
+ }
10746
+ runUpdate() {
10747
+ try {
10748
+ const result = this.updateDom();
10749
+ if (result && typeof result.then === "function") {
10750
+ return result.then(undefined, (error) => {
10751
+ this.updateError = error;
10752
+ });
9864
10753
  }
9865
- });
9866
- afterRecords.forEach(({ projection }) => {
9867
- projection.isPresent = true;
9868
- });
9869
- const root = getProjectionRoot(afterRecords, beforeRecords);
9870
- root?.didUpdate();
9871
- await new Promise((resolve) => {
9872
- frame.postRender(() => resolve());
9873
- });
9874
- const animations = collectAnimations(afterRecords);
9875
- const animation = new GroupAnimation(animations);
9876
- return animation;
10754
+ }
10755
+ catch (error) {
10756
+ this.updateError = error;
10757
+ }
10758
+ return undefined;
9877
10759
  }
9878
- buildRecords(elements) {
9879
- const records = [];
9880
- const recordMap = new Map();
9881
- for (const element of elements) {
9882
- const parentRecord = findParentRecord(element, recordMap, this.scope);
9883
- const { layout, layoutId } = readLayoutAttributes(element);
9884
- const override = layoutId
9885
- ? this.sharedTransitions.get(layoutId)
9886
- : undefined;
9887
- const transition = override || this.defaultOptions;
9888
- const record = getOrCreateRecord(element, parentRecord?.projection, {
9889
- layout,
9890
- layoutId,
9891
- animationType: typeof layout === "string" ? layout : "both",
9892
- transition: transition,
9893
- });
9894
- recordMap.set(element, record);
9895
- records.push(record);
10760
+ reconcileAdditions(newMemberIds) {
10761
+ for (const element of collectLayoutElements(this.scope)) {
10762
+ if (this.tracked.has(element))
10763
+ continue;
10764
+ const node = prepareNode(element, this.transitionFor(element));
10765
+ this.adopt(element, node);
10766
+ node.options.layoutId && newMemberIds.add(node.options.layoutId);
9896
10767
  }
9897
- return records;
9898
10768
  }
9899
- handleExitingElements(beforeRecords, afterRecords) {
9900
- const afterElementsSet = new Set(afterRecords.map((record) => record.element));
9901
- beforeRecords.forEach((record) => {
9902
- if (afterElementsSet.has(record.element))
10769
+ reconcileRemovals(newMemberIds) {
10770
+ this.tracked.forEach((node, element) => {
10771
+ if (element.isConnected)
9903
10772
  return;
9904
- // For shared layout elements, relegate to set up resumeFrom
9905
- // so the remaining element animates from this position
9906
- if (record.projection.options.layoutId) {
9907
- record.projection.isPresent = false;
9908
- record.projection.relegate();
9909
- }
9910
- record.visualElement.unmount();
9911
- visualElementStore.delete(record.element);
10773
+ const restore = this.restorePoints.get(element);
10774
+ this.restorePoints.delete(element);
10775
+ const { layoutId } = node.options;
10776
+ const stack = node.getStack();
10777
+ const hasSurvivor = stack &&
10778
+ stack.members.some((member) => member !== node &&
10779
+ member.instance
10780
+ ?.isConnected);
10781
+ /**
10782
+ * A removed lead with a surviving stack member - and no
10783
+ * replacement member added this commit - runs an exit
10784
+ * crossfade: restore the element to its old position in the
10785
+ * DOM, relegate it and let the survivor take over. It's
10786
+ * removed again once the animation completes.
10787
+ */
10788
+ if (layoutId &&
10789
+ node.isLead() &&
10790
+ hasSurvivor &&
10791
+ !newMemberIds.has(layoutId)) {
10792
+ if (restore && restore.parent.isConnected) {
10793
+ restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
10794
+ ? restore.next
10795
+ : null);
10796
+ node.isPresent = false;
10797
+ node.setOptions({
10798
+ onExitComplete: () => {
10799
+ element.remove();
10800
+ dropNode(element, node);
10801
+ },
10802
+ });
10803
+ if (node.relegate())
10804
+ return;
10805
+ element.remove();
10806
+ }
10807
+ }
10808
+ dropNode(element, node);
10809
+ this.tracked.delete(element);
9912
10810
  });
9913
- // Clear resumeFrom on EXISTING nodes that point to unmounted projections
9914
- // This prevents crossfade animation when the source element was removed entirely
9915
- // But preserve resumeFrom for NEW nodes so they can animate from the old position
9916
- // Also preserve resumeFrom for lead nodes that were just promoted via relegate
9917
- const beforeElementsSet = new Set(beforeRecords.map((record) => record.element));
9918
- afterRecords.forEach(({ element, projection }) => {
9919
- if (beforeElementsSet.has(element) &&
9920
- projection.resumeFrom &&
9921
- !projection.resumeFrom.instance &&
9922
- !projection.isLead()) {
9923
- projection.resumeFrom = undefined;
9924
- projection.snapshot = undefined;
10811
+ }
10812
+ getRoot() {
10813
+ let root;
10814
+ this.tracked.forEach((node) => (root || (root = node.root)));
10815
+ return root;
10816
+ }
10817
+ finalize() {
10818
+ if (this.updateError) {
10819
+ this.rejectReady(this.updateError);
10820
+ return;
10821
+ }
10822
+ const animations = new Set();
10823
+ this.tracked.forEach((node) => {
10824
+ if (node.instance && node.currentAnimation) {
10825
+ animations.add(node.currentAnimation);
9925
10826
  }
9926
10827
  });
10828
+ this.notifyReady(new GroupAnimation([...animations]));
9927
10829
  }
9928
10830
  }
9929
10831
  function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
9930
- // animateLayout(updateDom)
9931
10832
  if (typeof scopeOrUpdateDom === "function") {
9932
10833
  return {
9933
10834
  scope: document,
@@ -9935,107 +10836,15 @@
9935
10836
  defaultOptions: updateDomOrOptions,
9936
10837
  };
9937
10838
  }
9938
- // animateLayout(scope, updateDom, options?)
9939
- const elements = resolveElements(scopeOrUpdateDom);
9940
- const scope = elements[0] || document;
10839
+ const scope = scopeOrUpdateDom instanceof Document
10840
+ ? scopeOrUpdateDom
10841
+ : resolveElements(scopeOrUpdateDom)[0] ?? document;
9941
10842
  return {
9942
10843
  scope,
9943
10844
  updateDom: updateDomOrOptions,
9944
10845
  defaultOptions: options,
9945
10846
  };
9946
10847
  }
9947
- function collectLayoutElements(scope) {
9948
- const elements = Array.from(scope.querySelectorAll(layoutSelector));
9949
- if (scope instanceof Element && scope.matches(layoutSelector)) {
9950
- if (!elements.includes(scope)) {
9951
- elements.unshift(scope);
9952
- }
9953
- }
9954
- return elements;
9955
- }
9956
- function readLayoutAttributes(element) {
9957
- const layoutId = element.getAttribute("data-layout-id") || undefined;
9958
- const rawLayout = element.getAttribute("data-layout");
9959
- let layout;
9960
- if (rawLayout === "" || rawLayout === "true") {
9961
- layout = true;
9962
- }
9963
- else if (rawLayout) {
9964
- layout = rawLayout;
9965
- }
9966
- return {
9967
- layout,
9968
- layoutId,
9969
- };
9970
- }
9971
- function createVisualState() {
9972
- return {
9973
- latestValues: {},
9974
- renderState: {
9975
- transform: {},
9976
- transformOrigin: {},
9977
- style: {},
9978
- vars: {},
9979
- },
9980
- };
9981
- }
9982
- function getOrCreateRecord(element, parentProjection, projectionOptions) {
9983
- const existing = visualElementStore.get(element);
9984
- const visualElement = existing ??
9985
- new HTMLVisualElement({
9986
- props: {},
9987
- presenceContext: null,
9988
- visualState: createVisualState(),
9989
- }, { allowProjection: true });
9990
- if (!existing || !visualElement.projection) {
9991
- visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
9992
- }
9993
- visualElement.projection.setOptions({
9994
- ...projectionOptions,
9995
- visualElement,
9996
- });
9997
- if (!visualElement.current) {
9998
- visualElement.mount(element);
9999
- }
10000
- else if (!visualElement.projection.instance) {
10001
- // Mount projection if VisualElement is already mounted but projection isn't
10002
- // This happens when animate() was called before animateLayout()
10003
- visualElement.projection.mount(element);
10004
- }
10005
- if (!existing) {
10006
- visualElementStore.set(element, visualElement);
10007
- }
10008
- return {
10009
- element,
10010
- visualElement,
10011
- projection: visualElement.projection,
10012
- };
10013
- }
10014
- function findParentRecord(element, recordMap, scope) {
10015
- let parent = element.parentElement;
10016
- while (parent) {
10017
- const record = recordMap.get(parent);
10018
- if (record)
10019
- return record;
10020
- if (parent === scope)
10021
- break;
10022
- parent = parent.parentElement;
10023
- }
10024
- return undefined;
10025
- }
10026
- function getProjectionRoot(afterRecords, beforeRecords) {
10027
- const record = afterRecords[0] || beforeRecords[0];
10028
- return record?.projection.root;
10029
- }
10030
- function collectAnimations(afterRecords) {
10031
- const animations = new Set();
10032
- afterRecords.forEach((record) => {
10033
- const animation = record.projection.currentAnimation;
10034
- if (animation)
10035
- animations.add(animation);
10036
- });
10037
- return Array.from(animations);
10038
- }
10039
10848
 
10040
10849
  /**
10041
10850
  * @deprecated
@@ -11036,7 +11845,7 @@
11036
11845
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
11037
11846
  function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
11038
11847
  if (!container)
11039
- return noop$1;
11848
+ return noop;
11040
11849
  let containerHandlers = onScrollHandlers.get(container);
11041
11850
  /**
11042
11851
  * Get the onScroll handlers for this container.
@@ -11320,7 +12129,7 @@
11320
12129
 
11321
12130
  function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
11322
12131
  if (!container)
11323
- return noop$1;
12132
+ return noop;
11324
12133
  const optionsWithDefaults = { axis, container, ...options };
11325
12134
  return typeof onScroll === "function"
11326
12135
  ? attachToFunction(onScroll, optionsWithDefaults)
@@ -11400,7 +12209,6 @@
11400
12209
  exports.ViewTransitionBuilder = ViewTransitionBuilder;
11401
12210
  exports.VisualElement = VisualElement;
11402
12211
  exports.acceleratedValues = acceleratedValues;
11403
- exports.activeAnimations = activeAnimations;
11404
12212
  exports.addAttrValue = addAttrValue;
11405
12213
  exports.addDomEvent = addDomEvent;
11406
12214
  exports.addScaleCorrector = addScaleCorrector;
@@ -11426,6 +12234,7 @@
11426
12234
  exports.applyPointDelta = applyPointDelta;
11427
12235
  exports.applyPxDefaults = applyPxDefaults;
11428
12236
  exports.applyTreeDeltas = applyTreeDeltas;
12237
+ exports.arc = arc;
11429
12238
  exports.aspectRatio = aspectRatio;
11430
12239
  exports.attachFollow = attachFollow;
11431
12240
  exports.attachSpring = attachSpring;
@@ -11598,7 +12407,7 @@
11598
12407
  exports.motionValue = motionValue;
11599
12408
  exports.moveItem = moveItem;
11600
12409
  exports.nodeGroup = nodeGroup;
11601
- exports.noop = noop$1;
12410
+ exports.noop = noop;
11602
12411
  exports.number = number;
11603
12412
  exports.numberValueTypes = numberValueTypes;
11604
12413
  exports.observeTimeline = observeTimeline;