motion 14.6.0 → 14.7.1

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.
@@ -299,13 +299,17 @@
299
299
  const easingDefinitionToFunction = (definition) => {
300
300
  if (isBezierDefinition(definition)) {
301
301
  // If cubic bezier definition, create bezier curve
302
- exports.invariant(definition.length === 4, `Cubic bezier arrays must contain four numerical values.`, "cubic-bezier-length");
302
+ typeof process !== "undefined" &&
303
+ "development" !== "production" &&
304
+ exports.invariant(definition.length === 4, `Cubic bezier arrays must contain four numerical values.`, "cubic-bezier-length");
303
305
  const [x1, y1, x2, y2] = definition;
304
306
  return cubicBezier(x1, y1, x2, y2);
305
307
  }
306
308
  else if (isValidEasing(definition)) {
307
309
  // Else lookup from table
308
- exports.invariant(easingLookup[definition] !== undefined, `Invalid easing type '${definition}'`, "invalid-easing-type");
310
+ typeof process !== "undefined" &&
311
+ "development" !== "production" &&
312
+ exports.invariant(easingLookup[definition] !== undefined, `Invalid easing type '${definition}'`, "invalid-easing-type");
309
313
  return easingLookup[definition];
310
314
  }
311
315
  return definition;
@@ -442,34 +446,21 @@
442
446
  ")",
443
447
  };
444
448
 
449
+ /**
450
+ * #FF0000 has two characters per channel, #F00 one, which is doubled.
451
+ */
452
+ const channel = (v, i, size) => {
453
+ const c = v.substring(1 + i * size, 1 + (i + 1) * size);
454
+ return size > 1 ? c : c + c;
455
+ };
445
456
  function parseHex(v) {
446
- let r = "";
447
- let g = "";
448
- let b = "";
449
- let a = "";
450
- // If we have 6 characters, ie #FF0000
451
- if (v.length > 5) {
452
- r = v.substring(1, 3);
453
- g = v.substring(3, 5);
454
- b = v.substring(5, 7);
455
- a = v.substring(7, 9);
456
- // Or we have 3 characters, ie #F00
457
- }
458
- else {
459
- r = v.substring(1, 2);
460
- g = v.substring(2, 3);
461
- b = v.substring(3, 4);
462
- a = v.substring(4, 5);
463
- r += r;
464
- g += g;
465
- b += b;
466
- a += a;
467
- }
457
+ const size = v.length > 5 ? 2 : 1;
458
+ const alpha = channel(v, 3, size);
468
459
  return {
469
- red: parseInt(r, 16),
470
- green: parseInt(g, 16),
471
- blue: parseInt(b, 16),
472
- alpha: a ? parseInt(a, 16) / 255 : 1,
460
+ red: parseInt(channel(v, 0, size), 16),
461
+ green: parseInt(channel(v, 1, size), 16),
462
+ blue: parseInt(channel(v, 2, size), 16),
463
+ alpha: alpha ? parseInt(alpha, 16) / 255 : 1,
473
464
  };
474
465
  }
475
466
  const hex = {
@@ -846,7 +837,6 @@
846
837
  return orderedOrigin;
847
838
  }
848
839
  const mixComplex = (origin, target) => {
849
- const template = complex.createTransformer(target);
850
840
  const originStats = analyseComplexValue(origin);
851
841
  const targetStats = analyseComplexValue(target);
852
842
  const canInterpolate = originStats.indexes.var.length === targetStats.indexes.var.length &&
@@ -859,7 +849,7 @@
859
849
  !originStats.values.length)) {
860
850
  return mixVisibility(origin, target);
861
851
  }
862
- return pipe(mixArray(matchOrder(originStats, targetStats), targetStats.values), template);
852
+ return pipe(mixArray(matchOrder(originStats, targetStats), targetStats.values), buildTransformer(targetStats));
863
853
  }
864
854
  else {
865
855
  exports.warning(true, `Complex values '${origin}' and '${target}' too different to mix. Ensure all colors are of the same type, and that each contains the same quantity of number and color values. Falling back to instant transition.`, "complex-values-different");
@@ -1151,37 +1141,31 @@
1151
1141
  };
1152
1142
  }
1153
1143
 
1154
- const springDefaults = {
1155
- // Default spring physics
1156
- stiffness: 100,
1157
- damping: 10,
1158
- mass: 1.0,
1159
- // Default duration/bounce-based options
1160
- duration: 800, // in ms
1161
- bounce: 0.3,
1162
- visualDuration: 0.3, // in seconds
1163
- // Rest thresholds
1164
- restSpeed: {
1165
- granular: 0.01,
1166
- default: 2,
1167
- },
1168
- restDelta: {
1169
- granular: 0.005,
1170
- default: 0.5,
1171
- },
1172
- // Limits
1173
- minDuration: 0.01, // in seconds
1174
- maxDuration: 10.0, // in seconds
1175
- minDamping: 0.05,
1176
- };
1144
+ // Default spring physics
1145
+ const defaultStiffness = 100;
1146
+ const defaultDamping = 10;
1147
+ const defaultMass = 1.0;
1148
+ // Default duration/bounce-based options
1149
+ const defaultDuration = 800; // in ms
1150
+ const defaultBounce = 0.3;
1151
+ const defaultVisualDuration = 0.3; // in seconds
1152
+ // Rest thresholds
1153
+ const restSpeedGranular = 0.01;
1154
+ const restSpeedDefault = 2;
1155
+ const restDeltaGranular = 0.005;
1156
+ const restDeltaDefault = 0.5;
1157
+ // Limits
1158
+ const minDuration = 0.01; // in seconds
1159
+ const maxDuration = 10.0; // in seconds
1160
+ const minDamping = 0.05;
1177
1161
  /**
1178
1162
  * Maps bounce to a damping ratio as SwiftUI does: 0 is critically damped, and
1179
1163
  * it approaches undamped towards 1 and infinitely overdamped towards -1. Both
1180
1164
  * ends are limited so the spring still moves and settles.
1181
1165
  */
1182
1166
  const bounceToDampingRatio = (bounce) => bounce < 0
1183
- ? 1 / Math.max(1 + bounce, springDefaults.minDamping)
1184
- : Math.max(1 - bounce, springDefaults.minDamping);
1167
+ ? 1 / Math.max(1 + bounce, minDamping)
1168
+ : Math.max(1 - bounce, minDamping);
1185
1169
  /**
1186
1170
  * Scales an overdamped spring's undamped frequency so that, at its defining
1187
1171
  * time, it has as far left to go as the critically damped spring that's
@@ -1224,12 +1208,12 @@
1224
1208
  * ignore inherited velocity and only resolve without valid physics. Returns
1225
1209
  * NaN physics when the root search doesn't converge.
1226
1210
  */
1227
- function findSpring({ duration = springDefaults.duration, bounce = springDefaults.bounce, }) {
1211
+ function findSpring({ duration = defaultDuration, bounce = defaultBounce, }) {
1228
1212
  let envelope;
1229
1213
  let derivative;
1230
- exports.warning(duration <= secondsToMilliseconds(springDefaults.maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
1214
+ exports.warning(duration <= secondsToMilliseconds(maxDuration), "Spring duration must be 10 seconds or less", "spring-duration-limit");
1231
1215
  const dampingRatio = bounceToDampingRatio(bounce);
1232
- duration = clamp(springDefaults.minDuration, springDefaults.maxDuration, millisecondsToSeconds(duration));
1216
+ duration = clamp(minDuration, maxDuration, millisecondsToSeconds(duration));
1233
1217
  if (dampingRatio < 1) {
1234
1218
  /**
1235
1219
  * Underdamped spring
@@ -1314,9 +1298,9 @@
1314
1298
  const validMass = resolvePhysics(options.mass);
1315
1299
  const springOptions = {
1316
1300
  ...options,
1317
- stiffness: validStiffness ?? springDefaults.stiffness,
1318
- damping: validDamping ?? springDefaults.damping,
1319
- mass: validMass ?? springDefaults.mass,
1301
+ stiffness: validStiffness ?? defaultStiffness,
1302
+ damping: validDamping ?? defaultDamping,
1303
+ mass: validMass ?? defaultMass,
1320
1304
  isResolvedFromDuration: false,
1321
1305
  // stiffness/damping/mass overrides duration/bounce
1322
1306
  isTimeDefined: (validStiffness ?? validDamping ?? validMass) === undefined &&
@@ -1344,13 +1328,13 @@
1344
1328
  */
1345
1329
  if (!isValidPhysics(springOptions.stiffness) ||
1346
1330
  !isValidPhysics(springOptions.damping, true)) {
1347
- springOptions.stiffness = springDefaults.stiffness;
1348
- springOptions.damping = springDefaults.damping;
1331
+ springOptions.stiffness = defaultStiffness;
1332
+ springOptions.damping = defaultDamping;
1349
1333
  }
1350
1334
  }
1351
1335
  return springOptions;
1352
1336
  }
1353
- function springGenerator(optionsOrVisualDuration = springDefaults.visualDuration, bounce = springDefaults.bounce) {
1337
+ function springGenerator(optionsOrVisualDuration = defaultVisualDuration, bounce = defaultBounce) {
1354
1338
  const options = typeof optionsOrVisualDuration !== "object"
1355
1339
  ? {
1356
1340
  visualDuration: optionsOrVisualDuration,
@@ -1400,14 +1384,10 @@
1400
1384
  const isGranularScale = Math.abs(s.delta) < 5;
1401
1385
  s.restSpeed =
1402
1386
  options.restSpeed ||
1403
- (isGranularScale
1404
- ? springDefaults.restSpeed.granular
1405
- : springDefaults.restSpeed.default);
1387
+ (isGranularScale ? restSpeedGranular : restSpeedDefault);
1406
1388
  s.restDelta =
1407
1389
  options.restDelta ||
1408
- (isGranularScale
1409
- ? springDefaults.restDelta.granular
1410
- : springDefaults.restDelta.default);
1390
+ (isGranularScale ? restDeltaGranular : restDeltaDefault);
1411
1391
  };
1412
1392
  setRestThresholds();
1413
1393
  let resolveSpring;
@@ -1476,11 +1456,16 @@
1476
1456
  * dampedAngularFreq * t passes 300, both terms decay at the damping
1477
1457
  * rate. Heavily overdamped drag springs (dragElastic: 0,
1478
1458
  * dragTransition) rely on this to settle almost at once rather than
1479
- * creep at stiffness / damping. Negative bounce needs the exact
1480
- * curve, so time-defined springs are exempt.
1459
+ * creep at stiffness / damping. It only applies when reached within
1460
+ * 300ms (a dampedAngularFreq of 1 rad/ms), so it reads as a stiff
1461
+ * spring snapping into place: any later and it cuts off a slow spring
1462
+ * that's visibly still moving (#1207). Negative bounce needs the
1463
+ * exact curve, so time-defined springs are exempt.
1481
1464
  */
1482
- const limit = isTimeDefined ? Infinity : 300 / dampedAngularFreq;
1483
- const decayAt = (rate, t) => Math.exp(t > limit ? -rate * limit - decay * (t - limit) : -rate * t);
1465
+ const limit = dampedAngularFreq < 1 || isTimeDefined
1466
+ ? Infinity
1467
+ : 300 / dampedAngularFreq;
1468
+ const decayAt = (rate, t) => Math.exp(t > limit ? (decay - rate) * limit - decay * t : -rate * t);
1484
1469
  const c = { S: 0, F: 0 };
1485
1470
  update = () => {
1486
1471
  const P = (s.velocity + decay * s.delta) / dampedAngularFreq;
@@ -1561,7 +1546,7 @@
1561
1546
  };
1562
1547
  let amplitude = power * velocity;
1563
1548
  const ideal = origin + amplitude;
1564
- const target = modifyTarget === undefined ? ideal : modifyTarget(ideal);
1549
+ const target = modifyTarget ? modifyTarget(ideal) : ideal;
1565
1550
  /**
1566
1551
  * If the target has changed we need to re-calculate the amplitude, otherwise
1567
1552
  * the animation will start from the wrong position.
@@ -1572,7 +1557,8 @@
1572
1557
  const applyFriction = (t) => {
1573
1558
  const delta = calcDelta(t);
1574
1559
  state.done = Math.abs(delta) <= restDelta;
1575
- state.value = state.done ? target : target + delta;
1560
+ // target + delta can be off from origin by a float error at t = 0
1561
+ state.value = state.done ? target : t ? target + delta : origin;
1576
1562
  };
1577
1563
  /**
1578
1564
  * Ideally this would resolve for t in a stateless way, we could
@@ -2018,17 +2004,12 @@
2018
2004
  this.generator = generator;
2019
2005
  }
2020
2006
  updateTime(timestamp) {
2021
- const animationTime = Math.round(timestamp - this.startTime) * this.playbackSpeed;
2022
- // Update currentTime
2023
- if (this.holdTime !== null) {
2024
- this.currentTime = this.holdTime;
2025
- }
2026
- else {
2027
- // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
2028
- // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
2029
- // example.
2030
- this.currentTime = animationTime;
2031
- }
2007
+ // Rounding the time because floating point arithmetic is not always accurate, e.g. 3000.367 - 1000.367 =
2008
+ // 2000.0000000000002. This is a problem when we are comparing the currentTime with the duration, for
2009
+ // example.
2010
+ this.currentTime =
2011
+ this.holdTime ??
2012
+ Math.round(timestamp - this.startTime) * this.playbackSpeed;
2032
2013
  }
2033
2014
  tick(timestamp, sample = false) {
2034
2015
  const { generator, totalDuration, calculatedDuration } = this;
@@ -2069,10 +2050,17 @@
2069
2050
  }
2070
2051
  const state = this.generate(this.currentTime, isInDelayPhase);
2071
2052
  let { done } = state;
2072
- if (!isInDelayPhase && calculatedDuration !== null) {
2053
+ /**
2054
+ * A generator that takes longer than maxGeneratorDuration to finish
2055
+ * has an Infinity duration, so played forwards it finishes when it
2056
+ * says it's done.
2057
+ */
2058
+ if (!isInDelayPhase) {
2073
2059
  done =
2074
2060
  this.playbackSpeed >= 0
2075
- ? this.currentTime >= totalDuration
2061
+ ? calculatedDuration < Infinity
2062
+ ? this.currentTime >= totalDuration
2063
+ : done
2076
2064
  : this.currentTime <= 0;
2077
2065
  }
2078
2066
  const isAnimationFinished = this.holdTime === null &&
@@ -2178,20 +2166,11 @@
2178
2166
  }
2179
2167
  return state;
2180
2168
  }
2181
- /**
2182
- * Allows the returned animation to be awaited or promise-chained. Currently
2183
- * resolves when the animation finishes at all but in a future update could/should
2184
- * reject if its cancels.
2185
- */
2186
- then(resolve, reject) {
2187
- return this.finished.then(resolve, reject);
2188
- }
2189
2169
  get duration() {
2190
2170
  return millisecondsToSeconds(this.calculatedDuration);
2191
2171
  }
2192
2172
  get iterationDuration() {
2193
- const { delay = 0 } = this.options || {};
2194
- return this.duration + millisecondsToSeconds(delay);
2173
+ return this.duration + millisecondsToSeconds(this.options.delay ?? 0);
2195
2174
  }
2196
2175
  get time() {
2197
2176
  return millisecondsToSeconds(this.currentTime);
@@ -2295,14 +2274,9 @@
2295
2274
  }
2296
2275
  teardown() {
2297
2276
  this.state = "idle";
2298
- this.stopDriver();
2299
- this.startTime = this.holdTime = null;
2300
- }
2301
- stopDriver() {
2302
- if (!this.driver)
2303
- return;
2304
- this.driver.stop();
2277
+ this.driver?.stop();
2305
2278
  this.driver = undefined;
2279
+ this.startTime = this.holdTime = null;
2306
2280
  }
2307
2281
  sample(sampleTime) {
2308
2282
  this.startTime = 0;
@@ -2559,23 +2533,15 @@
2559
2533
  if (!transform || transform === "none") {
2560
2534
  return defaultTransformValue(name);
2561
2535
  }
2562
- const matrix3dMatch = transform.match(/^matrix3d\(([-\d.e\s,]+)\)$/u);
2563
- let parsers;
2564
- let match;
2565
- if (matrix3dMatch) {
2566
- parsers = matrix3dParsers;
2567
- match = matrix3dMatch;
2568
- }
2569
- else {
2570
- const matrix2dMatch = transform.match(/^matrix\(([-\d.e\s,]+)\)$/u);
2571
- parsers = matrix2dParsers;
2572
- match = matrix2dMatch;
2573
- }
2536
+ const match = transform.match(/^matrix(3d)?\(([-\d.e\s,]+)\)$/u);
2574
2537
  if (!match) {
2575
2538
  return defaultTransformValue(name);
2576
2539
  }
2577
- const valueParser = parsers[name];
2578
- const values = match[1].split(",").map(convertTransformToNumber);
2540
+ const valueParser = (match[1] ? matrix3dParsers : matrix2dParsers)[name];
2541
+ /**
2542
+ * parseFloat skips leading whitespace and stops at trailing whitespace.
2543
+ */
2544
+ const values = match[2].split(",").map((v) => parseFloat(v));
2579
2545
  return typeof valueParser === "function"
2580
2546
  ? valueParser(values)
2581
2547
  : values[valueParser];
@@ -2584,9 +2550,6 @@
2584
2550
  const { transform = "none" } = getComputedStyle(instance);
2585
2551
  return parseValueFromTransform(transform, name);
2586
2552
  };
2587
- function convertTransformToNumber(value) {
2588
- return parseFloat(value.trim());
2589
- }
2590
2553
 
2591
2554
  /**
2592
2555
  * Generate a list of every possible transform key.
@@ -2989,7 +2952,9 @@
2989
2952
  this.isPseudoElement = Boolean(pseudoElement);
2990
2953
  this.allowFlatten = allowFlatten;
2991
2954
  this.options = options;
2992
- exports.invariant(typeof options.type !== "string", `Mini animate() doesn't support "type" as a string.`, "mini-spring");
2955
+ typeof process !== "undefined" &&
2956
+ "development" !== "production" &&
2957
+ exports.invariant(typeof options.type !== "string", `Mini animate() doesn't support "type" as a string.`, "mini-spring");
2993
2958
  const transition = applyGeneratorOptions(options);
2994
2959
  this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
2995
2960
  if (transition.autoplay === false) {
@@ -2999,9 +2964,7 @@
2999
2964
  this.finishedTime = this.time;
3000
2965
  if (!pseudoElement) {
3001
2966
  const keyframe = getFinalKeyframe(keyframes, this.options, finalKeyframe, this.speed);
3002
- if (this.updateMotionValue) {
3003
- this.updateMotionValue(keyframe);
3004
- }
2967
+ this.updateMotionValue?.(keyframe);
3005
2968
  /**
3006
2969
  * If we can, we want to commit the final style as set by the user,
3007
2970
  * rather than the computed keyframe value supplied by the animation.
@@ -3322,6 +3285,7 @@
3322
3285
  if (!isOriginAnimatable || !isTargetAnimatable) {
3323
3286
  // Only build the message when we're going to warn
3324
3287
  isOriginAnimatable !== isTargetAnimatable &&
3288
+ "development" !== "production" &&
3325
3289
  exports.warning(false, `You are trying to animate ${name} from "${originKeyframe}" to "${targetKeyframe}". "${isOriginAnimatable ? targetKeyframe : originKeyframe}" is not an animatable value.`, "value-not-animatable");
3326
3290
  return false;
3327
3291
  }
@@ -3336,13 +3300,16 @@
3336
3300
 
3337
3301
  /**
3338
3302
  * A list of values that can be hardware-accelerated.
3303
+ *
3304
+ * Colors aren't included: WAAPI mixes them in sRGB, but JS animations
3305
+ * (and the sampler that estimates a WAAPI value on interrupt) mix them
3306
+ * in linear color space, so interrupting one would visibly jump.
3339
3307
  */
3340
3308
  const acceleratedValues = new Set([
3341
3309
  "opacity",
3342
3310
  "clipPath",
3343
3311
  "filter",
3344
3312
  "transform",
3345
- "backgroundColor",
3346
3313
  ]);
3347
3314
 
3348
3315
  const browserColorFunctions = /^(?:oklch|oklab|lab|lch|color|color-mix|light-dark)\(/;
@@ -3434,6 +3401,20 @@
3434
3401
  transformPerspective: "perspective",
3435
3402
  };
3436
3403
  const numTransforms = transformPropOrder.length;
3404
+ /**
3405
+ * Cache of `translateX(`-style openers so each transform function is a few
3406
+ * string concatenations rather than a template.
3407
+ */
3408
+ const openers = {};
3409
+ /**
3410
+ * A transform function, e.g. `translateX(10px)` for `x: 10`.
3411
+ */
3412
+ const transformFunction = (key, value, valueAsType = getValueAsType(value, transformValueTypes[key])) => (openers[key] || (openers[key] = (translateAlias[key] || key) + "(")) + valueAsType + ")";
3413
+ /**
3414
+ * Whether a transform value is at its default, so can be left out.
3415
+ */
3416
+ const isDefaultTransform = (key, value) => (typeof value === "number" ? value : parseFloat(value)) ===
3417
+ (key.startsWith("scale") ? 1 : 0);
3437
3418
  /**
3438
3419
  * Build a CSS transform style from individual x/y/scale etc properties.
3439
3420
  *
@@ -3443,7 +3424,6 @@
3443
3424
  function buildTransform$1(latestValues, transform, transformTemplate) {
3444
3425
  // The transform string we're going to build into.
3445
3426
  let transformString = "";
3446
- let transformIsDefault = true;
3447
3427
  /**
3448
3428
  * Loop over all possible transforms in order, adding the ones that
3449
3429
  * are present to the transform string.
@@ -3453,24 +3433,11 @@
3453
3433
  const value = latestValues[key];
3454
3434
  if (value === undefined)
3455
3435
  continue;
3456
- let valueIsDefault = true;
3457
- if (typeof value === "number") {
3458
- valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
3459
- }
3460
- else {
3461
- const parsed = parseFloat(value);
3462
- valueIsDefault = key.startsWith("scale") ? parsed === 1 : parsed === 0;
3463
- }
3464
- if (!valueIsDefault || transformTemplate) {
3465
- const valueAsType = getValueAsType(value, numberValueTypes[key]);
3466
- if (!valueIsDefault) {
3467
- transformIsDefault = false;
3468
- const transformName = translateAlias[key] || key;
3469
- transformString += `${transformName}(${valueAsType}) `;
3470
- }
3471
- if (transformTemplate) {
3472
- transform[key] = valueAsType;
3473
- }
3436
+ const valueAsType = transformTemplate &&
3437
+ (transform[key] = getValueAsType(value, numberValueTypes[key]));
3438
+ if (!isDefaultTransform(key, value)) {
3439
+ transformString +=
3440
+ transformFunction(key, value, valueAsType || undefined) + " ";
3474
3441
  }
3475
3442
  }
3476
3443
  // `pathRotation` composes onto `rotate` as a separate additive term so
@@ -3478,19 +3445,14 @@
3478
3445
  // `transformPropOrder`.
3479
3446
  const pathRotation = latestValues.pathRotation;
3480
3447
  if (pathRotation) {
3481
- transformIsDefault = false;
3482
- transformString += `rotate(${getValueAsType(pathRotation, numberValueTypes.pathRotation)}) `;
3448
+ transformString += transformFunction("rotate", pathRotation);
3483
3449
  }
3484
3450
  transformString = transformString.trim();
3485
3451
  // If we have a custom `transform` template, pass our transform values and
3486
3452
  // generated transformString to that before returning
3487
- if (transformTemplate) {
3488
- transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
3489
- }
3490
- else if (transformIsDefault) {
3491
- transformString = "none";
3492
- }
3493
- return transformString;
3453
+ return transformTemplate
3454
+ ? transformTemplate(transform, transformString)
3455
+ : transformString || "none";
3494
3456
  }
3495
3457
 
3496
3458
  const transformGroups = new WeakMap();
@@ -3832,27 +3794,30 @@
3832
3794
  return values;
3833
3795
  };
3834
3796
  /**
3835
- * Build the transform at a timestamp. Unlike buildTransform, every key
3836
- * is written, even at its default, so each keyframe has the same list
3837
- * of functions and the browser interpolates them one by one.
3797
+ * Build a transform from values in transformPropOrder. For a keyframe,
3798
+ * every key is written, even at its default, so each keyframe has the
3799
+ * same list of functions and the browser interpolates them one by one.
3800
+ * As an inline style, defaults are left out, as buildTransform does.
3838
3801
  */
3839
- const compose = (timestamp, settled) => {
3802
+ const toTransform = (values, isStyle) => {
3840
3803
  let transform = "";
3841
- const values = valuesAt(timestamp, settled);
3842
3804
  for (const key in values) {
3843
- transform +=
3844
- (translateAlias[key] || key) +
3845
- "(" +
3846
- getValueAsType(values[key], numberValueTypes[key]) +
3847
- ") ";
3805
+ const value = values[key];
3806
+ if (!isStyle || !isDefaultTransform(key, value)) {
3807
+ transform += transformFunction(key, value) + " ";
3808
+ }
3848
3809
  }
3849
- return transform || "none";
3810
+ return (isStyle ? transform.trim() : transform) || "none";
3850
3811
  };
3812
+ /**
3813
+ * Build the transform at a timestamp.
3814
+ */
3815
+ const compose = (timestamp, settled) => toTransform(valuesAt(timestamp, settled));
3851
3816
  /**
3852
3817
  * Write the transform as an inline style, as the renderer would.
3853
3818
  */
3854
3819
  const commit = (values = valuesAt()) => {
3855
- element.style.transform = buildTransform$1(values, {});
3820
+ element.style.transform = toTransform(values, true);
3856
3821
  };
3857
3822
  /**
3858
3823
  * Cancel the WAAPI animations, by default first committing the
@@ -3863,6 +3828,13 @@
3863
3828
  segments.forEach(({ animation }) => animation.cancel());
3864
3829
  segments = [];
3865
3830
  };
3831
+ /**
3832
+ * A WAAPI animation that add() starts by setting its startTime. Playing
3833
+ * it first, as element.animate() does, would leave WebKit holding that
3834
+ * play's ready time once startTime ends the play, and it uses that for
3835
+ * the next updatePlaybackRate(), so the animation would jump.
3836
+ */
3837
+ const create = (keyframes, options) => new Animation(new KeyframeEffect(element, keyframes, options));
3866
3838
  const add = (animation, offset, end = Infinity) => {
3867
3839
  const segment = { animation, offset, end };
3868
3840
  animation.startTime = time.now() + offset;
@@ -4055,7 +4027,7 @@
4055
4027
  commit(valuesAt(from, settled));
4056
4028
  keyframes = null;
4057
4029
  }
4058
- return element.animate(keyframes, {
4030
+ return create(keyframes, {
4059
4031
  ...options,
4060
4032
  duration: to - from,
4061
4033
  easing: easing || "linear",
@@ -4145,9 +4117,7 @@
4145
4117
  * empty animations that end with them.
4146
4118
  */
4147
4119
  ends.forEach((trackEnd) => trackEnd < end - 1 &&
4148
- add(element.animate(null, {
4149
- duration: trackEnd - now,
4150
- }), 0, trackEnd - now));
4120
+ add(create(null, { duration: trackEnd - now }), 0, trackEnd - now));
4151
4121
  }
4152
4122
  if (cycle) {
4153
4123
  /**
@@ -4221,12 +4191,29 @@
4221
4191
  if (isDone)
4222
4192
  continue;
4223
4193
  const local = elapsed - offset;
4224
- if (rate) {
4194
+ if (rate &&
4195
+ animation.updatePlaybackRate &&
4196
+ animation.playState === "running" &&
4197
+ Math.abs(Number(animation.currentTime) - local) < 20) {
4198
+ /**
4199
+ * Only the speed changed. Setting playbackRate and
4200
+ * startTime restarts a running compositor animation, which
4201
+ * can render the underlying value for a frame.
4202
+ */
4203
+ animation.playbackRate !== rate &&
4204
+ animation.updatePlaybackRate(rate);
4205
+ }
4206
+ else if (rate) {
4225
4207
  animation.playbackRate = rate;
4226
4208
  animation.startTime = now - local / rate;
4227
4209
  }
4228
4210
  else {
4229
- animation.pause();
4211
+ /**
4212
+ * Hold it without pause(), whose pending pause setting
4213
+ * currentTime would end at once, leaving WebKit holding its
4214
+ * ready time (see create()).
4215
+ */
4216
+ animation.startTime = null;
4230
4217
  animation.currentTime = local;
4231
4218
  }
4232
4219
  }
@@ -4373,6 +4360,7 @@
4373
4360
  built = new Map();
4374
4361
  stop();
4375
4362
  tracks.forEach((track) => track.state === "running" && track.js?.start());
4363
+ return true;
4376
4364
  },
4377
4365
  };
4378
4366
  transformGroups.set(element, group);
@@ -4417,9 +4405,8 @@
4417
4405
  * If we can't animate this value with the resolved keyframes
4418
4406
  * then we should complete it immediately.
4419
4407
  */
4420
- let canAnimateValue = true;
4421
- if (!canAnimate(keyframes, name, type, velocity)) {
4422
- canAnimateValue = false;
4408
+ const canAnimateValue = canAnimate(keyframes, name, type, velocity);
4409
+ if (!canAnimateValue) {
4423
4410
  if (MotionGlobalConfig.instantAnimations || !delay) {
4424
4411
  onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
4425
4412
  }
@@ -4563,9 +4550,7 @@
4563
4550
  this.animation.complete();
4564
4551
  }
4565
4552
  cancel() {
4566
- if (this._animation) {
4567
- this.animation.cancel();
4568
- }
4553
+ this._animation?.cancel();
4569
4554
  this.keyframeResolver?.cancel();
4570
4555
  }
4571
4556
  }
@@ -4748,7 +4733,7 @@
4748
4733
  this.setCurrent(v);
4749
4734
  // Update update subscribers
4750
4735
  if (this.current !== this.prev) {
4751
- this.notifyChange();
4736
+ this.dirty();
4752
4737
  if (this.dependents) {
4753
4738
  for (const dependent of this.dependents) {
4754
4739
  dependent.dirty();
@@ -4886,12 +4871,9 @@
4886
4871
  * @public
4887
4872
  */
4888
4873
  set(v) {
4889
- if (!this.passiveEffect) {
4890
- this.updateAndNotify(v);
4891
- }
4892
- else {
4893
- this.passiveEffect(v, this.updateAndNotify);
4894
- }
4874
+ this.passiveEffect
4875
+ ? this.passiveEffect(v, this.updateAndNotify)
4876
+ : this.updateAndNotify(v);
4895
4877
  }
4896
4878
  setWithVelocity(prev, current, delta) {
4897
4879
  this.set(current);
@@ -4908,13 +4890,9 @@
4908
4890
  this.prev = v;
4909
4891
  this.prevUpdatedAt = this.prevFrameValue = undefined;
4910
4892
  endAnimation && this.stop();
4911
- if (this.stopPassiveEffect)
4912
- this.stopPassiveEffect();
4893
+ this.stopPassiveEffect?.();
4913
4894
  }
4914
4895
  dirty() {
4915
- this.notifyChange();
4916
- }
4917
- notifyChange() {
4918
4896
  const { current, changeSubscriber } = this;
4919
4897
  /**
4920
4898
  * One or the other exists. Subscribing during the direct
@@ -4929,15 +4907,10 @@
4929
4907
  }
4930
4908
  }
4931
4909
  addDependent(dependent) {
4932
- if (!this.dependents) {
4933
- this.dependents = new Set();
4934
- }
4935
- this.dependents.add(dependent);
4910
+ (this.dependents || (this.dependents = new Set())).add(dependent);
4936
4911
  }
4937
4912
  removeDependent(dependent) {
4938
- if (this.dependents) {
4939
- this.dependents.delete(dependent);
4940
- }
4913
+ this.dependents?.delete(dependent);
4941
4914
  }
4942
4915
  /**
4943
4916
  * Returns the latest state of `MotionValue`
@@ -4947,9 +4920,7 @@
4947
4920
  * @public
4948
4921
  */
4949
4922
  get() {
4950
- if (collectMotionValues.current) {
4951
- collectMotionValues.current.push(this);
4952
- }
4923
+ collectMotionValues.current?.push(this);
4953
4924
  /**
4954
4925
  * A value animating on the compositor isn't updated every frame,
4955
4926
  * so read it from its animation.
@@ -5015,7 +4986,7 @@
5015
4986
  isComplete = true;
5016
4987
  this.events.animationComplete?.notify();
5017
4988
  if (this.animation === animation)
5018
- this.clearAnimation();
4989
+ this.animation = undefined;
5019
4990
  resolve();
5020
4991
  });
5021
4992
  if (!isComplete)
@@ -5031,11 +5002,10 @@
5031
5002
  stop() {
5032
5003
  if (this.animation) {
5033
5004
  this.animation.stop();
5034
- if (this.events.animationCancel) {
5035
- this.events.animationCancel.notify();
5036
- }
5005
+ this.events.animationCancel?.notify();
5037
5006
  }
5038
- this.clearAnimation();
5007
+ // Assign rather than delete to keep the object's shape stable
5008
+ this.animation = undefined;
5039
5009
  }
5040
5010
  /**
5041
5011
  * Returns `true` if this value is currently animating.
@@ -5045,10 +5015,6 @@
5045
5015
  isAnimating() {
5046
5016
  return !!this.animation;
5047
5017
  }
5048
- clearAnimation() {
5049
- // Assign rather than delete to keep the object's shape stable
5050
- this.animation = undefined;
5051
- }
5052
5018
  /**
5053
5019
  * Destroy and clean up subscribers to this `MotionValue`.
5054
5020
  *
@@ -5063,9 +5029,7 @@
5063
5029
  this.events.destroy?.notify();
5064
5030
  this.clearListeners();
5065
5031
  this.stop();
5066
- if (this.stopPassiveEffect) {
5067
- this.stopPassiveEffect();
5068
- }
5032
+ this.stopPassiveEffect?.();
5069
5033
  }
5070
5034
  }
5071
5035
  function motionValue(init, options) {
@@ -5143,6 +5107,7 @@
5143
5107
  "repeatDelay",
5144
5108
  "from",
5145
5109
  "elapsed",
5110
+ "out",
5146
5111
  ]);
5147
5112
  /**
5148
5113
  * Decide whether a transition is defined on a given Transition.
@@ -5613,7 +5578,7 @@
5613
5578
  }
5614
5579
  const valueTransition = {
5615
5580
  delay,
5616
- ...getValueTransition$1(transition || {}, key),
5581
+ ...getValueTransition$1(animationTypeState?.out[key] || transition || {}, key),
5617
5582
  };
5618
5583
  if (skipAnimations)
5619
5584
  valueTransition.skipAnimations = true;
@@ -5783,8 +5748,9 @@
5783
5748
  }
5784
5749
 
5785
5750
  class DOMKeyframesResolver extends KeyframeResolver {
5786
- constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
5787
- super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
5751
+ constructor() {
5752
+ super(...arguments);
5753
+ this.isAsync = true;
5788
5754
  }
5789
5755
  /**
5790
5756
  * Keyframes that don't need reading from the DOM, or measuring,
@@ -5812,7 +5778,7 @@
5812
5778
  }
5813
5779
  readKeyframes() {
5814
5780
  const { unresolvedKeyframes, element, name } = this;
5815
- if (!element || !element.current)
5781
+ if (!element?.current)
5816
5782
  return;
5817
5783
  super.readKeyframes();
5818
5784
  /**
@@ -5907,11 +5873,10 @@
5907
5873
  }
5908
5874
  measureInitialState() {
5909
5875
  const { element, unresolvedKeyframes, name } = this;
5910
- if (!element || !element.current)
5876
+ if (!element?.current)
5911
5877
  return;
5912
- if (name === "height") {
5878
+ if (name === "height")
5913
5879
  this.suspendedScrollY = window.pageYOffset;
5914
- }
5915
5880
  this.measuredOrigin = this.measure();
5916
5881
  unresolvedKeyframes[0] = this.measuredOrigin;
5917
5882
  // Set final key frame to measure after next render
@@ -5922,7 +5887,7 @@
5922
5887
  }
5923
5888
  measureEndState() {
5924
5889
  const { element, unresolvedKeyframes } = this;
5925
- if (!element || !element.current)
5890
+ if (!element?.current)
5926
5891
  return;
5927
5892
  this.motionValue?.jump(this.measuredOrigin, false);
5928
5893
  const finalKeyframeIndex = unresolvedKeyframes.length - 1;
@@ -5931,14 +5896,6 @@
5931
5896
  if (finalKeyframe !== null && this.finalKeyframe === undefined) {
5932
5897
  this.finalKeyframe = finalKeyframe;
5933
5898
  }
5934
- // If we removed transform values, reapply them before the next render
5935
- if (this.removedTransforms?.length) {
5936
- this.removedTransforms.forEach(([unsetTransformName, unsetTransformValue]) => {
5937
- element
5938
- .getValue(unsetTransformName)
5939
- .set(unsetTransformValue);
5940
- });
5941
- }
5942
5899
  this.resolveNoneKeyframes();
5943
5900
  }
5944
5901
  }
@@ -6043,7 +6000,8 @@
6043
6000
  */
6044
6001
  const effects = [];
6045
6002
  function addEffect(effect) {
6046
- exports.invariant(typeof effect.test === "function" && typeof effect.read === "function", "Effects passed to animate.addEffect() need test() and read().", "effect-missing-test");
6003
+ exports.invariant(typeof effect.test === "function" &&
6004
+ typeof effect.read === "function", "Effects passed to animate.addEffect() need test() and read().", "effect-missing-test");
6047
6005
  removeEffect(effect);
6048
6006
  effects.unshift(effect);
6049
6007
  }
@@ -6370,11 +6328,6 @@
6370
6328
  });
6371
6329
  }
6372
6330
 
6373
- /**
6374
- * Cache of `translateX(`-style openers so each render is a few string
6375
- * concatenations rather than a template per transform.
6376
- */
6377
- const openers = {};
6378
6331
  function buildTransform(state) {
6379
6332
  let transform = "";
6380
6333
  const { transformKeys: keys = [], transformValues: values = {} } = state;
@@ -6387,14 +6340,8 @@
6387
6340
  const value = values[key].getCurrent();
6388
6341
  if (value === undefined)
6389
6342
  continue;
6390
- const parsed = typeof value === "number" ? value : parseFloat(value);
6391
- if (parsed !== (key.startsWith("scale") ? 1 : 0)) {
6392
- transform +=
6393
- (transform && " ") +
6394
- (openers[key] ||
6395
- (openers[key] = (translateAlias[key] || key) + "(")) +
6396
- getValueAsType(value, transformValueTypes[key]) +
6397
- ")";
6343
+ if (!isDefaultTransform(key, value)) {
6344
+ transform += (transform && " ") + transformFunction(key, value);
6398
6345
  }
6399
6346
  }
6400
6347
  // See build-transform.ts: additive `rotate()` so user `rotate` isn't
@@ -6402,10 +6349,7 @@
6402
6349
  const pathRotation = state.get("pathRotation")?.get();
6403
6350
  if (pathRotation) {
6404
6351
  transform +=
6405
- (transform && " ") +
6406
- "rotate(" +
6407
- getValueAsType(pathRotation, transformValueTypes.pathRotation) +
6408
- ")";
6352
+ (transform && " ") + transformFunction("rotate", pathRotation);
6409
6353
  }
6410
6354
  return transform || "none";
6411
6355
  }
@@ -6808,10 +6752,24 @@
6808
6752
  !isIdentityScale(scaleX) ||
6809
6753
  !isIdentityScale(scaleY));
6810
6754
  }
6755
+ /**
6756
+ * Rotations and skews, which projection can't correct for.
6757
+ */
6758
+ const angleTransforms = [
6759
+ "rotate",
6760
+ "rotateX",
6761
+ "rotateY",
6762
+ "rotateZ",
6763
+ "skewX",
6764
+ "skewY",
6765
+ ];
6811
6766
  function hasTransform(values) {
6812
6767
  return (hasScale(values) ||
6813
6768
  has2DTranslate(values) ||
6814
- values.z ||
6769
+ hasDistortingTransform(values));
6770
+ }
6771
+ function hasDistortingTransform(values) {
6772
+ return (values.z ||
6815
6773
  values.rotate ||
6816
6774
  values.rotateX ||
6817
6775
  values.rotateY ||
@@ -6864,6 +6822,11 @@
6864
6822
  * in a tree upon our box before then calculating how to project it into our desired viewport-relative box
6865
6823
  *
6866
6824
  * This is the final nested loop within updateLayoutDelta for future refactoring
6825
+ *
6826
+ * isSharedTransition is true for a shared layout animation in page space, which
6827
+ * also applies ancestor scroll and transforms. Or it's the layoutRoot a shared
6828
+ * animation is relative to, so only transforms of ancestors not shared with it
6829
+ * (or above it) are applied.
6867
6830
  */
6868
6831
  function applyTreeDeltas(box, treeScale, treePath, isSharedTransition = false) {
6869
6832
  const treeLength = treePath.length;
@@ -6880,13 +6843,10 @@
6880
6843
  * TODO: Prefer to remove this, but currently we have motion components with
6881
6844
  * display: contents in Framer.
6882
6845
  */
6883
- const { visualElement } = node.options;
6884
- if (visualElement &&
6885
- visualElement.props.style &&
6886
- visualElement.props.style.display === "contents") {
6846
+ if (node.options.visualElement?.props.style?.display === "contents") {
6887
6847
  continue;
6888
6848
  }
6889
- if (isSharedTransition &&
6849
+ if (isSharedTransition === true &&
6890
6850
  node.options.layoutScroll &&
6891
6851
  node.scroll &&
6892
6852
  node !== node.root) {
@@ -6900,7 +6860,9 @@
6900
6860
  // Apply each ancestor's calculated delta into this component's recorded layout box
6901
6861
  applyBoxDelta(box, delta);
6902
6862
  }
6903
- if (isSharedTransition && hasTransform(node.latestValues)) {
6863
+ if (isSharedTransition &&
6864
+ (isSharedTransition.path?.[i] || isSharedTransition) !== node &&
6865
+ hasTransform(node.latestValues)) {
6904
6866
  transformBox(box, node.latestValues, node.layout?.layoutBox);
6905
6867
  }
6906
6868
  }
@@ -7167,15 +7129,6 @@
7167
7129
  const asNumber = (v) => typeof v === "number" ? v : parseFloat(v);
7168
7130
 
7169
7131
  const numBorders = cornerRadiusProps.length;
7170
- const mixedTransforms = [
7171
- "rotate",
7172
- "rotateX",
7173
- "rotateY",
7174
- "rotateZ",
7175
- "skewX",
7176
- "skewY",
7177
- ];
7178
- const numMixedTransforms = mixedTransforms.length;
7179
7132
  const isPx = (value) => typeof value === "number" || px.test(value);
7180
7133
  function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
7181
7134
  if (shouldCrossfadeOpacity) {
@@ -7213,8 +7166,8 @@
7213
7166
  * Mix rotation and skew. These can't be projected, so mix them every
7214
7167
  * frame rather than leaving them frozen at the animation-start snapshot.
7215
7168
  */
7216
- for (let i = 0; i < numMixedTransforms; i++) {
7217
- const key = mixedTransforms[i];
7169
+ for (let i = 0; i < angleTransforms.length; i++) {
7170
+ const key = angleTransforms[i];
7218
7171
  if (follow[key] || lead[key]) {
7219
7172
  target[key] = mixNumber$1(follow[key] || 0, lead[key] || 0, progress);
7220
7173
  }
@@ -7297,8 +7250,8 @@
7297
7250
  }
7298
7251
  }
7299
7252
  function calcBoxDelta(delta, source, target, origin) {
7300
- calcAxisDelta(delta.x, source.x, target.x, origin ? origin.originX : undefined);
7301
- calcAxisDelta(delta.y, source.y, target.y, origin ? origin.originY : undefined);
7253
+ calcAxisDelta(delta.x, source.x, target.x, origin?.originX);
7254
+ calcAxisDelta(delta.y, source.y, target.y, origin?.originY);
7302
7255
  }
7303
7256
  function calcRelativeAxis(target, relative, parent, anchor = 0) {
7304
7257
  const anchorPoint = anchor
@@ -7368,8 +7321,8 @@
7368
7321
  * and acts as a bridge between motion values and removeAxisDelta
7369
7322
  */
7370
7323
  function removeBoxTransforms(box, transforms, originBox, sourceBox) {
7371
- removeAxisTransforms(box.x, transforms, xKeys, originBox ? originBox.x : undefined, sourceBox ? sourceBox.x : undefined);
7372
- removeAxisTransforms(box.y, transforms, yKeys, originBox ? originBox.y : undefined, sourceBox ? sourceBox.y : undefined);
7324
+ removeAxisTransforms(box.x, transforms, xKeys, originBox?.x, sourceBox?.x);
7325
+ removeAxisTransforms(box.y, transforms, yKeys, originBox?.y, sourceBox?.y);
7373
7326
  }
7374
7327
 
7375
7328
  const createAxisDelta = () => ({
@@ -7599,24 +7552,18 @@
7599
7552
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
7600
7553
  }
7601
7554
  if (latestTransform) {
7602
- const { transformPerspective, rotate, pathRotation, rotateX, rotateY, rotateZ, skewX, skewY, } = latestTransform;
7555
+ const { transformPerspective } = latestTransform;
7603
7556
  if (transformPerspective)
7604
7557
  transform = `perspective(${transformPerspective}px) ${transform}`;
7605
- if (rotate)
7606
- transform += `rotate(${rotate}deg) `;
7607
- // Additive `rotate()` so user `rotate` isn't clobbered.
7608
- if (pathRotation)
7609
- transform += `rotate(${pathRotation}deg) `;
7610
- if (rotateX)
7611
- transform += `rotateX(${rotateX}deg) `;
7612
- if (rotateY)
7613
- transform += `rotateY(${rotateY}deg) `;
7614
- if (rotateZ)
7615
- transform += `rotateZ(${rotateZ}deg) `;
7616
- if (skewX)
7617
- transform += `skewX(${skewX}deg) `;
7618
- if (skewY)
7619
- transform += `skewY(${skewY}deg) `;
7558
+ for (let i = 0; i < angleTransforms.length; i++) {
7559
+ const key = angleTransforms[i];
7560
+ const value = latestTransform[key];
7561
+ if (value)
7562
+ transform += `${key}(${value}deg) `;
7563
+ // Additive `rotate()` so user `rotate` isn't clobbered.
7564
+ if (!i && latestTransform.pathRotation)
7565
+ transform += `rotate(${latestTransform.pathRotation}deg) `;
7566
+ }
7620
7567
  }
7621
7568
  /**
7622
7569
  * Apply scale to match the size of the element to the size we want it.
@@ -7679,7 +7626,10 @@
7679
7626
  calculatedTargetDeltas: 0,
7680
7627
  calculatedProjections: 0,
7681
7628
  };
7682
- const transformAxes = ["", "X", "Y", "Z"];
7629
+ /**
7630
+ * Transforms that distort an element's layout, so must be reset before measuring.
7631
+ */
7632
+ const distortingTransforms = ["z", "skew", "skewZ"];
7683
7633
  /**
7684
7634
  * We use 1000 as the animation target as 0-1000 maps better to pixels than 0-1
7685
7635
  * which has a noticeable difference in spring animations
@@ -7697,7 +7647,15 @@
7697
7647
  }
7698
7648
  }
7699
7649
  }
7700
- const demoteTransforms = ({ instance }) => instance && transformGroups.get(instance)?.demote();
7650
+ /**
7651
+ * Move accelerated transforms back to the main thread. The WAAPI animation
7652
+ * hid startUpdate()'s reset of rotations, skews and z, and moving off it
7653
+ * writes the whole transform back, so reset them again before this node
7654
+ * is measured.
7655
+ */
7656
+ const demoteTransforms = (node) => node.instance &&
7657
+ transformGroups.get(node.instance)?.demote() &&
7658
+ node.resetSkewAndRotation();
7701
7659
  function cancelTreeOptimisedTransformAnimations(projectionNode) {
7702
7660
  projectionNode.hasCheckedOptimisedAppear = true;
7703
7661
  if (projectionNode.root === projectionNode)
@@ -7887,8 +7845,7 @@
7887
7845
  return this.eventHandlers.get(name).add(handler);
7888
7846
  }
7889
7847
  notifyListeners(name, ...args) {
7890
- const subscriptionManager = this.eventHandlers.get(name);
7891
- subscriptionManager && subscriptionManager.notify(...args);
7848
+ this.eventHandlers.get(name)?.notify(...args);
7892
7849
  }
7893
7850
  hasListeners(name) {
7894
7851
  return this.eventHandlers.has(name);
@@ -7906,7 +7863,7 @@
7906
7863
  visualElement.mount(instance);
7907
7864
  }
7908
7865
  this.root.nodes.add(this);
7909
- this.parent && this.parent.children.add(this);
7866
+ this.parent?.children.add(this);
7910
7867
  if (this.root.hasTreeAnimated && (layout || layoutId)) {
7911
7868
  this.isLayoutDirty = true;
7912
7869
  }
@@ -7924,7 +7881,7 @@
7924
7881
  return;
7925
7882
  innerWidth = newInnerWidth;
7926
7883
  this.root.updateBlockedByResize = true;
7927
- cancelDelay && cancelDelay();
7884
+ cancelDelay?.();
7928
7885
  cancelDelay = delay(resizeUnblockUpdate, 250);
7929
7886
  if (globalProjectionState.hasAnimatedSinceResize) {
7930
7887
  globalProjectionState.hasAnimatedSinceResize = false;
@@ -8015,9 +7972,7 @@
8015
7972
  if (!hasLayoutChanged) {
8016
7973
  finishAnimation(this);
8017
7974
  }
8018
- if (this.isLead() && this.options.onExitComplete) {
8019
- this.options.onExitComplete();
8020
- }
7975
+ this.isLead() && this.options.onExitComplete?.();
8021
7976
  }
8022
7977
  this.targetLayout = newLayout;
8023
7978
  });
@@ -8025,9 +7980,8 @@
8025
7980
  unmount() {
8026
7981
  this.options.layoutId && this.willUpdate();
8027
7982
  this.root.nodes.remove(this);
8028
- const stack = this.getStack();
8029
- stack && stack.remove(this);
8030
- this.parent && this.parent.children.delete(this);
7983
+ this.getStack()?.remove(this);
7984
+ this.parent?.children.delete(this);
8031
7985
  this.instance = undefined;
8032
7986
  this.eventHandlers.clear();
8033
7987
  this.hasLayoutListener = false;
@@ -8048,7 +8002,7 @@
8048
8002
  if (this.isUpdateBlocked())
8049
8003
  return;
8050
8004
  this.isUpdating = true;
8051
- this.nodes && this.nodes.forEach(resetSkewAndRotation);
8005
+ this.nodes?.forEach(resetSkewAndRotation);
8052
8006
  this.animationId++;
8053
8007
  }
8054
8008
  getTransformTemplate() {
@@ -8058,7 +8012,7 @@
8058
8012
  willUpdate(shouldNotifyListeners = true) {
8059
8013
  this.root.hasTreeAnimated = true;
8060
8014
  if (this.root.isUpdateBlocked()) {
8061
- this.options.onExitComplete && this.options.onExitComplete();
8015
+ this.options.onExitComplete?.();
8062
8016
  return;
8063
8017
  }
8064
8018
  /**
@@ -8275,24 +8229,23 @@
8275
8229
  this.projectionDelta = undefined;
8276
8230
  this.notifyListeners("measure", this.layout.layoutBox);
8277
8231
  const { visualElement } = this.options;
8278
- visualElement &&
8279
- visualElement.notify("LayoutMeasure", this.layout.layoutBox, prevLayout ? prevLayout.layoutBox : undefined);
8232
+ visualElement?.notify("LayoutMeasure", this.layout.layoutBox, prevLayout?.layoutBox);
8280
8233
  }
8281
8234
  updateScroll(phase = "measure") {
8282
- let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
8283
- if (this.scroll &&
8284
- this.scroll.animationId === this.root.animationId &&
8285
- this.scroll.phase === phase) {
8286
- needsMeasurement = false;
8287
- }
8288
- if (needsMeasurement && this.instance) {
8289
- const isRoot = checkIsScrollRoot(this.instance);
8235
+ const { scroll, instance } = this;
8236
+ const { animationId } = this.root;
8237
+ if (this.options.layoutScroll &&
8238
+ instance &&
8239
+ !(scroll &&
8240
+ scroll.animationId === animationId &&
8241
+ scroll.phase === phase)) {
8242
+ const isRoot = checkIsScrollRoot(instance);
8290
8243
  this.scroll = {
8291
- animationId: this.root.animationId,
8244
+ animationId,
8292
8245
  phase,
8293
8246
  isRoot,
8294
- offset: measureScroll(this.instance),
8295
- wasRoot: this.scroll ? this.scroll.isRoot : isRoot,
8247
+ offset: measureScroll(instance),
8248
+ wasRoot: scroll ? scroll.isRoot : isRoot,
8296
8249
  };
8297
8250
  }
8298
8251
  }
@@ -8382,10 +8335,10 @@
8382
8335
  }
8383
8336
  return boxWithoutScroll;
8384
8337
  }
8385
- applyTransform(box, transformOnly = false, output, skipOwnTransform) {
8338
+ applyTransform(box, transformOnly = false, output, skipOwnTransform, start = 0) {
8386
8339
  const withTransforms = output || createBox();
8387
8340
  copyBoxInto(withTransforms, box);
8388
- for (let i = 0; i < this.path.length; i++) {
8341
+ for (let i = start; i < this.path.length; i++) {
8389
8342
  const node = this.path[i];
8390
8343
  if (!transformOnly &&
8391
8344
  node.options.layoutScroll &&
@@ -8557,8 +8510,13 @@
8557
8510
  this.attemptToResolveRelativeTarget = false;
8558
8511
  if (this.options.layoutAnchor !== false &&
8559
8512
  relativeParent &&
8560
- Boolean(relativeParent.resumingFrom) ===
8561
- Boolean(this.resumingFrom) &&
8513
+ (!relativeParent.resumingFrom === !this.resumingFrom ||
8514
+ /**
8515
+ * Shared layout animations within a layoutRoot
8516
+ * animate relative to it.
8517
+ */
8518
+ (relativeParent.options.layoutRoot &&
8519
+ !relativeParent.resumingFrom)) &&
8562
8520
  !relativeParent.options.layoutScroll &&
8563
8521
  relativeParent.target &&
8564
8522
  this.animationProgress !== 1) {
@@ -8638,7 +8596,7 @@
8638
8596
  * If this section of the tree isn't animating we can
8639
8597
  * delete our target sources for the following frame.
8640
8598
  */
8641
- this.isTreeAnimating = Boolean((this.parent && this.parent.isTreeAnimating) ||
8599
+ this.isTreeAnimating = Boolean(this.parent?.isTreeAnimating ||
8642
8600
  this.currentAnimation ||
8643
8601
  this.pendingAnimation);
8644
8602
  if (!this.isTreeAnimating) {
@@ -8659,8 +8617,17 @@
8659
8617
  /**
8660
8618
  * Apply all the parent deltas to this box to produce the corrected box. This
8661
8619
  * is the layout box, as it will appear on screen as a result of the transforms of its parents.
8620
+ *
8621
+ * A shared target is usually in page space, so ancestor transforms are applied
8622
+ * too. But a target relative to a parent that isn't itself shared (like a
8623
+ * layoutRoot) is in that parent's layout space. So the transforms of the
8624
+ * parent and its ancestors, which move both elements alike, are skipped,
8625
+ * but those of other ancestors (between it and either element) apply.
8662
8626
  */
8663
- applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared);
8627
+ applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared &&
8628
+ (lead.relativeTarget && !lead.relativeParent.resumingFrom
8629
+ ? lead.relativeParent
8630
+ : true));
8664
8631
  /**
8665
8632
  * If this layer needs to perform scale correction but doesn't have a target,
8666
8633
  * use the layout as the target.
@@ -8727,8 +8694,7 @@
8727
8694
  scheduleRender(notifyAll = true) {
8728
8695
  this.options.visualElement?.scheduleRender();
8729
8696
  if (notifyAll) {
8730
- const stack = this.getStack();
8731
- stack && stack.scheduleRender();
8697
+ this.getStack()?.scheduleRender();
8732
8698
  }
8733
8699
  if (this.resumingFrom && !this.resumingFrom.instance) {
8734
8700
  this.resumingFrom = undefined;
@@ -8750,9 +8716,7 @@
8750
8716
  }
8751
8717
  this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
8752
8718
  const relativeLayout = createBox();
8753
- const snapshotSource = snapshot ? snapshot.source : undefined;
8754
- const layoutSource = this.layout ? this.layout.source : undefined;
8755
- const isSharedLayoutAnimation = snapshotSource !== layoutSource;
8719
+ const isSharedLayoutAnimation = snapshot?.source !== this.layout?.source;
8756
8720
  const stack = this.getStack();
8757
8721
  const isOnlyMember = !stack || stack.members.length <= 1;
8758
8722
  const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
@@ -8767,19 +8731,11 @@
8767
8731
  this.mixTargetDelta = (latest) => {
8768
8732
  const progress = latest / 1000;
8769
8733
  const point = interpolate?.(progress);
8734
+ mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
8735
+ mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
8770
8736
  if (point) {
8771
8737
  targetDelta.x.translate = point.x;
8772
- targetDelta.x.scale = mixNumber$1(delta.x.scale, 1, progress);
8773
- targetDelta.x.origin = delta.x.origin;
8774
- targetDelta.x.originPoint = delta.x.originPoint;
8775
8738
  targetDelta.y.translate = point.y;
8776
- targetDelta.y.scale = mixNumber$1(delta.y.scale, 1, progress);
8777
- targetDelta.y.origin = delta.y.origin;
8778
- targetDelta.y.originPoint = delta.y.originPoint;
8779
- }
8780
- else {
8781
- mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
8782
- mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
8783
8739
  }
8784
8740
  this.setTargetDelta(targetDelta);
8785
8741
  if (this.relativeTarget &&
@@ -8805,7 +8761,7 @@
8805
8761
  this.animationValues = mixedValues;
8806
8762
  mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
8807
8763
  }
8808
- if (point && point.rotate !== undefined) {
8764
+ if (point?.rotate !== undefined) {
8809
8765
  // Dedicated `pathRotation` channel, not `rotate`, so an
8810
8766
  // animating `rotate` is composed with, never clobbered.
8811
8767
  if (!this.animationValues)
@@ -8870,7 +8826,7 @@
8870
8826
  }
8871
8827
  finishAnimation() {
8872
8828
  if (this.currentAnimation) {
8873
- this.mixTargetDelta && this.mixTargetDelta(animationTarget);
8829
+ this.mixTargetDelta?.(animationTarget);
8874
8830
  this.currentAnimation.stop();
8875
8831
  }
8876
8832
  this.completeAnimation();
@@ -8890,13 +8846,11 @@
8890
8846
  this.layout &&
8891
8847
  layout &&
8892
8848
  shouldAnimatePositionOnly(this.options.animationType, this.layout.layoutBox, layout.layoutBox)) {
8849
+ const { layoutBox } = this.layout;
8850
+ const leadTarget = target;
8893
8851
  target = this.target || createBox();
8894
- const xLength = calcLength(this.layout.layoutBox.x);
8895
- target.x.min = lead.target.x.min;
8896
- target.x.max = target.x.min + xLength;
8897
- const yLength = calcLength(this.layout.layoutBox.y);
8898
- target.y.min = lead.target.y.min;
8899
- target.y.max = target.y.min + yLength;
8852
+ alignAxis(target.x, leadTarget.x, layoutBox.x);
8853
+ alignAxis(target.y, leadTarget.y, layoutBox.y);
8900
8854
  }
8901
8855
  copyBoxInto(targetWithTransforms, target);
8902
8856
  /**
@@ -8921,8 +8875,8 @@
8921
8875
  stack.add(node);
8922
8876
  const config = node.options.initialPromotionConfig;
8923
8877
  node.promote({
8924
- transition: config ? config.transition : undefined,
8925
- preserveFollowOpacity: config && config.shouldPreserveFollowOpacity
8878
+ transition: config?.transition,
8879
+ preserveFollowOpacity: config?.shouldPreserveFollowOpacity
8926
8880
  ? config.shouldPreserveFollowOpacity(node)
8927
8881
  : undefined,
8928
8882
  });
@@ -8945,9 +8899,7 @@
8945
8899
  return this.root.sharedNodes.get(layoutId);
8946
8900
  }
8947
8901
  promote({ needsReset, transition, preserveFollowOpacity, } = {}) {
8948
- const stack = this.getStack();
8949
- if (stack)
8950
- stack.promote(this, preserveFollowOpacity);
8902
+ this.getStack()?.promote(this, preserveFollowOpacity);
8951
8903
  if (needsReset) {
8952
8904
  this.projectionDelta = undefined;
8953
8905
  this.needsReset = true;
@@ -8956,45 +8908,27 @@
8956
8908
  this.setOptions({ transition });
8957
8909
  }
8958
8910
  relegate() {
8959
- const stack = this.getStack();
8960
- if (stack) {
8961
- return stack.relegate(this);
8962
- }
8963
- else {
8964
- return false;
8965
- }
8911
+ return this.getStack()?.relegate(this) || false;
8966
8912
  }
8967
8913
  resetSkewAndRotation() {
8968
8914
  const { visualElement } = this.options;
8969
8915
  if (!visualElement)
8970
8916
  return;
8971
- // If there's no detected skew or rotation values, we can early return without a forced render.
8972
- let hasDistortingTransform = false;
8973
8917
  /**
8974
8918
  * An unrolled check for rotation values. Most elements don't have any rotation and
8975
- * skipping the nested loop and new object creation is 50% faster.
8919
+ * skipping the loop and new object creation is 50% faster.
8920
+ * If there's no distorting values, we don't need to do any more.
8976
8921
  */
8977
8922
  const { latestValues } = visualElement;
8978
- if (latestValues.z ||
8979
- latestValues.rotate ||
8980
- latestValues.rotateX ||
8981
- latestValues.rotateY ||
8982
- latestValues.rotateZ ||
8983
- latestValues.skewX ||
8984
- latestValues.skewY) {
8985
- hasDistortingTransform = true;
8986
- }
8987
- // If there's no distorting values, we don't need to do any more.
8988
- if (!hasDistortingTransform)
8923
+ if (!hasDistortingTransform(latestValues))
8989
8924
  return;
8990
8925
  const resetValues = {};
8991
- if (latestValues.z) {
8992
- resetDistortingTransform("z", visualElement, resetValues, this.animationValues);
8993
- }
8994
8926
  // Check the skew and rotate value of all axes and reset to 0
8995
- for (let i = 0; i < transformAxes.length; i++) {
8996
- resetDistortingTransform(`rotate${transformAxes[i]}`, visualElement, resetValues, this.animationValues);
8997
- resetDistortingTransform(`skew${transformAxes[i]}`, visualElement, resetValues, this.animationValues);
8927
+ for (let i = 0; i < distortingTransforms.length; i++) {
8928
+ resetDistortingTransform(distortingTransforms[i], visualElement, resetValues, this.animationValues);
8929
+ }
8930
+ for (let i = 0; i < angleTransforms.length; i++) {
8931
+ resetDistortingTransform(angleTransforms[i], visualElement, resetValues, this.animationValues);
8998
8932
  }
8999
8933
  // Force a render of this element to apply the transform with all skews and rotations
9000
8934
  // set to 0.
@@ -9014,7 +8948,13 @@
9014
8948
  styleProp) {
9015
8949
  if (!this.instance || this.isSVG)
9016
8950
  return;
9017
- if (!this.isVisible) {
8951
+ const lead = this.getLead();
8952
+ /**
8953
+ * Without crossfade, a new lead hides the previous one, as it's
8954
+ * usually unmounting. One that gave its layoutId away keeps
8955
+ * rendering, so only hide nodes that follow another.
8956
+ */
8957
+ if (!this.isVisible && lead !== this) {
9018
8958
  targetStyle.visibility = "hidden";
9019
8959
  return;
9020
8960
  }
@@ -9030,9 +8970,10 @@
9030
8970
  : "none";
9031
8971
  return;
9032
8972
  }
9033
- const lead = this.getLead();
9034
8973
  if (!this.projectionDelta || !this.layout || !lead.target) {
9035
- if (this.options.layoutId) {
8974
+ if (this.options.layoutId || this.needsShow) {
8975
+ this.needsShow = false;
8976
+ targetStyle.visibility = "";
9036
8977
  targetStyle.opacity =
9037
8978
  this.latestValues.opacity !== undefined
9038
8979
  ? this.latestValues.opacity
@@ -9166,52 +9107,49 @@
9166
9107
  const isShared = snapshot.source !== node.layout.source;
9167
9108
  // TODO Maybe we want to also resize the layout snapshot so we don't trigger
9168
9109
  // animations for instance if layout="size" and an element has only changed position
9169
- if (animationType === "size") {
9170
- eachAxis((axis) => {
9171
- const axisSnapshot = isShared
9172
- ? snapshot.measuredBox[axis]
9173
- : snapshot.layoutBox[axis];
9174
- const length = calcLength(axisSnapshot);
9175
- axisSnapshot.min = layout[axis].min;
9176
- axisSnapshot.max = axisSnapshot.min + length;
9177
- });
9178
- }
9179
- else if (animationType === "x" || animationType === "y") {
9180
- const snapAxis = animationType === "x" ? "y" : "x";
9181
- copyAxisInto(isShared
9182
- ? snapshot.measuredBox[snapAxis]
9183
- : snapshot.layoutBox[snapAxis], layout[snapAxis]);
9184
- }
9185
- else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
9186
- eachAxis((axis) => {
9187
- const axisSnapshot = isShared
9188
- ? snapshot.measuredBox[axis]
9189
- : snapshot.layoutBox[axis];
9190
- const length = calcLength(layout[axis]);
9191
- axisSnapshot.max = axisSnapshot.min + length;
9192
- /**
9193
- * Ensure relative target gets resized and rerendererd
9194
- */
9195
- if (node.relativeTarget && !node.currentAnimation) {
9196
- node.isProjectionDirty = true;
9197
- node.relativeTarget[axis].max =
9198
- node.relativeTarget[axis].min + length;
9199
- }
9200
- });
9201
- }
9110
+ const resizeSnapshot = (box, to) => {
9111
+ if (animationType === "size") {
9112
+ eachAxis((axis) => {
9113
+ const length = calcLength(box[axis]);
9114
+ box[axis].min = to[axis].min;
9115
+ box[axis].max = box[axis].min + length;
9116
+ });
9117
+ }
9118
+ else if (animationType === "x" || animationType === "y") {
9119
+ const snapAxis = animationType === "x" ? "y" : "x";
9120
+ copyAxisInto(box[snapAxis], to[snapAxis]);
9121
+ }
9122
+ else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
9123
+ eachAxis((axis) => {
9124
+ const length = calcLength(to[axis]);
9125
+ box[axis].max = box[axis].min + length;
9126
+ /**
9127
+ * Ensure relative target gets resized and rerendererd
9128
+ */
9129
+ if (node.relativeTarget && !node.currentAnimation) {
9130
+ node.isProjectionDirty = true;
9131
+ node.relativeTarget[axis].max =
9132
+ node.relativeTarget[axis].min + length;
9133
+ }
9134
+ });
9135
+ }
9136
+ };
9137
+ const snapshotBox = isShared ? snapshot.measuredBox : snapshot.layoutBox;
9138
+ resizeSnapshot(snapshotBox, layout);
9202
9139
  const layoutDelta = createDelta();
9203
9140
  calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
9204
9141
  const visualDelta = createDelta();
9205
- if (isShared) {
9206
- calcBoxDelta(visualDelta, node.applyTransform(measuredLayout, true), snapshot.measuredBox);
9207
- }
9208
- else {
9209
- calcBoxDelta(visualDelta, layout, snapshot.layoutBox);
9210
- }
9142
+ calcBoxDelta(visualDelta, isShared ? node.applyTransform(measuredLayout, true) : layout, snapshotBox);
9211
9143
  const hasLayoutChanged = !isDeltaZero(layoutDelta);
9212
9144
  let hasRelativeLayoutChanged = false;
9213
- if (!node.resumeFrom) {
9214
- const relativeParent = node.getClosestProjectingParent();
9145
+ const relativeParent = node.getClosestProjectingParent();
9146
+ /**
9147
+ * A snapshot resumed from another element is only relative to this
9148
+ * node's relativeParent if it's a layoutRoot that contains both.
9149
+ */
9150
+ if (!node.resumeFrom ||
9151
+ (relativeParent?.options.layoutRoot &&
9152
+ node.resumeFrom.path.includes(relativeParent))) {
9215
9153
  /**
9216
9154
  * If the relativeParent is itself resuming from a different element then
9217
9155
  * the relative snapshot is not relavent
@@ -9220,10 +9158,22 @@
9220
9158
  const { snapshot: parentSnapshot, layout: parentLayout } = relativeParent;
9221
9159
  if (parentSnapshot && parentLayout) {
9222
9160
  const anchor = node.options.layoutAnchor || undefined;
9161
+ /**
9162
+ * A shared snapshot includes the transforms of the old
9163
+ * element's ancestors below the layoutRoot, as they
9164
+ * aren't shared with the new element.
9165
+ */
9223
9166
  const relativeSnapshot = createBox();
9224
- calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
9167
+ calcRelativePosition(relativeSnapshot, isShared
9168
+ ? node.resumeFrom.applyTransform(snapshot.layoutBox, true, undefined, true, relativeParent.depth + 1)
9169
+ : snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
9225
9170
  const relativeLayout = createBox();
9226
9171
  calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox, anchor);
9172
+ /**
9173
+ * A shared snapshot is resized in page space, above, so
9174
+ * resize it relative to the layoutRoot too.
9175
+ */
9176
+ isShared && resizeSnapshot(relativeSnapshot, relativeLayout);
9227
9177
  if (!boxEqualsRounded(relativeSnapshot, relativeLayout)) {
9228
9178
  hasRelativeLayoutChanged = true;
9229
9179
  }
@@ -9245,8 +9195,7 @@
9245
9195
  });
9246
9196
  }
9247
9197
  else if (node.isLead()) {
9248
- const { onExitComplete } = node.options;
9249
- onExitComplete && onExitComplete();
9198
+ node.options.onExitComplete?.();
9250
9199
  }
9251
9200
  /**
9252
9201
  * Clearing transition
@@ -9352,7 +9301,7 @@
9352
9301
  }
9353
9302
  function resetTransformStyle(node) {
9354
9303
  const { visualElement } = node.options;
9355
- if (visualElement && visualElement.getProps().onBeforeLayoutMeasure) {
9304
+ if (visualElement?.getProps().onBeforeLayoutMeasure) {
9356
9305
  visualElement.notify("BeforeLayoutMeasure");
9357
9306
  }
9358
9307
  node.resetTransform();
@@ -9386,7 +9335,7 @@
9386
9335
  mixAxis(output.y, from.y, to.y, p);
9387
9336
  }
9388
9337
  function hasOpacityCrossfade(node) {
9389
- return (node.animationValues && node.animationValues.opacityExit !== undefined);
9338
+ return node.animationValues?.opacityExit !== undefined;
9390
9339
  }
9391
9340
  const defaultLayoutTransition = {
9392
9341
  duration: 0.45,
@@ -9412,6 +9361,13 @@
9412
9361
  roundAxis(box.x);
9413
9362
  roundAxis(box.y);
9414
9363
  }
9364
+ /**
9365
+ * Align an axis to the start of another, keeping the length of a layout axis.
9366
+ * The length is read first, as output can be the layout axis itself.
9367
+ */
9368
+ function alignAxis(output, from, layout) {
9369
+ output.max = calcLength(layout) + (output.min = from.min);
9370
+ }
9415
9371
  function shouldAnimatePositionOnly(animationType, snapshot, layout) {
9416
9372
  return (animationType === "position" ||
9417
9373
  (animationType === "preserve-aspect" &&
@@ -9462,7 +9418,7 @@
9462
9418
  resetTransform: (instance, value) => {
9463
9419
  instance.style.transform = value !== undefined ? value : "none";
9464
9420
  },
9465
- checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9421
+ checkIsScrollRoot: (instance) => window.getComputedStyle(instance).position === "fixed",
9466
9422
  });
9467
9423
 
9468
9424
  const visualElementStore = new WeakMap();
@@ -9748,9 +9704,7 @@
9748
9704
  }
9749
9705
  this.current = instance;
9750
9706
  visualElementStore.set(instance, this);
9751
- if (this.projection && !this.projection.instance) {
9752
- this.projection.mount(instance);
9753
- }
9707
+ this.projection?.instance || this.projection?.mount(instance);
9754
9708
  /**
9755
9709
  * Join the closest variant node above, so its variant changes
9756
9710
  * propagate here, unless inherit={false} cuts the chain.
@@ -9796,12 +9750,12 @@
9796
9750
  this.hasBeenMounted = true;
9797
9751
  }
9798
9752
  unmount() {
9799
- this.projection && this.projection.unmount();
9753
+ this.projection?.unmount();
9800
9754
  cancelFrame(this.notifyUpdate);
9801
9755
  cancelFrame(this.render);
9802
9756
  this.valueSubscriptions.forEach((remove) => remove());
9803
9757
  this.valueSubscriptions.clear();
9804
- this.removeFromVariantTree && this.removeFromVariantTree();
9758
+ this.removeFromVariantTree?.();
9805
9759
  this.parent?.removeChild(this);
9806
9760
  for (const key in this.events) {
9807
9761
  this.events[key].clear();
@@ -9817,17 +9771,14 @@
9817
9771
  }
9818
9772
  addChild(child) {
9819
9773
  this.children.add(child);
9820
- this.enteringChildren ?? (this.enteringChildren = new Set());
9821
- this.enteringChildren.add(child);
9774
+ (this.enteringChildren || (this.enteringChildren = new Set())).add(child);
9822
9775
  }
9823
9776
  removeChild(child) {
9824
9777
  this.children.delete(child);
9825
- this.enteringChildren && this.enteringChildren.delete(child);
9778
+ this.enteringChildren?.delete(child);
9826
9779
  }
9827
9780
  bindToMotionValue(key, value) {
9828
- if (this.valueSubscriptions.has(key)) {
9829
- this.valueSubscriptions.get(key)();
9830
- }
9781
+ this.valueSubscriptions.get(key)?.();
9831
9782
  if (value.accelerate &&
9832
9783
  acceleratedValues.has(key) &&
9833
9784
  this.current instanceof HTMLElement) {
@@ -9869,8 +9820,7 @@
9869
9820
  }
9870
9821
  this.valueSubscriptions.set(key, () => {
9871
9822
  removeOnChange();
9872
- if (removeSyncCheck)
9873
- removeSyncCheck();
9823
+ removeSyncCheck?.();
9874
9824
  // Defer to MotionValue.on("change") auto-stop so React 19 remounts
9875
9825
  // can resubscribe before the animation is cancelled (#3315).
9876
9826
  });
@@ -9963,10 +9913,8 @@
9963
9913
  */
9964
9914
  for (let i = 0; i < propEventHandlers.length; i++) {
9965
9915
  const key = propEventHandlers[i];
9966
- if (this.propEventSubscriptions[key]) {
9967
- this.propEventSubscriptions[key]();
9968
- delete this.propEventSubscriptions[key];
9969
- }
9916
+ this.propEventSubscriptions[key]?.();
9917
+ delete this.propEventSubscriptions[key];
9970
9918
  const listenerName = ("on" + key);
9971
9919
  const listener = props[listenerName];
9972
9920
  if (listener) {
@@ -9974,9 +9922,7 @@
9974
9922
  }
9975
9923
  }
9976
9924
  this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps || {}, this), this.prevMotionValues);
9977
- if (this.handleChildMotionValue) {
9978
- this.handleChildMotionValue();
9979
- }
9925
+ this.handleChildMotionValue?.();
9980
9926
  }
9981
9927
  getProps() {
9982
9928
  return this.props;
@@ -9985,7 +9931,7 @@
9985
9931
  * Returns the variant definition with a given name.
9986
9932
  */
9987
9933
  getVariant(name) {
9988
- return this.props.variants ? this.props.variants[name] : undefined;
9934
+ return this.props.variants?.[name];
9989
9935
  }
9990
9936
  /**
9991
9937
  * Returns the defined default transition on this component.
@@ -10023,11 +9969,8 @@
10023
9969
  */
10024
9970
  removeValue(key) {
10025
9971
  this.values.delete(key);
10026
- const unsubscribe = this.valueSubscriptions.get(key);
10027
- if (unsubscribe) {
10028
- unsubscribe();
10029
- this.valueSubscriptions.delete(key);
10030
- }
9972
+ this.valueSubscriptions.get(key)?.();
9973
+ this.valueSubscriptions.delete(key);
10031
9974
  delete this.latestValues[key];
10032
9975
  this.removeValueFromRenderState(key, this.renderState);
10033
9976
  }
@@ -10038,10 +9981,10 @@
10038
9981
  return this.values.has(key);
10039
9982
  }
10040
9983
  getValue(key, defaultValue) {
10041
- if (this.props.values && this.props.values[key]) {
10042
- return this.props.values[key];
10043
- }
10044
- let value = this.values.get(key);
9984
+ let value = this.props.values?.[key];
9985
+ if (value)
9986
+ return value;
9987
+ value = this.values.get(key);
10045
9988
  if (value === undefined && defaultValue !== undefined) {
10046
9989
  value = motionValue(defaultValue ?? this.getDefaultValue?.(key), {
10047
9990
  owner: this,
@@ -10091,15 +10034,11 @@
10091
10034
  this.baseTarget[key] = value;
10092
10035
  }
10093
10036
  on(eventName, callback) {
10094
- if (!this.events[eventName]) {
10095
- this.events[eventName] = new SubscriptionManager();
10096
- }
10097
- return this.events[eventName].add(callback);
10037
+ return (this.events[eventName] ||
10038
+ (this.events[eventName] = new SubscriptionManager())).add(callback);
10098
10039
  }
10099
10040
  notify(eventName, ...args) {
10100
- if (this.events[eventName]) {
10101
- this.events[eventName].notify(...args);
10102
- }
10041
+ this.events[eventName]?.notify(...args);
10103
10042
  }
10104
10043
  scheduleRenderMicrotask() {
10105
10044
  microtask.render(this.render);
@@ -10272,7 +10211,7 @@
10272
10211
  let ancestor = element.parentElement;
10273
10212
  while (ancestor) {
10274
10213
  const node = layoutNodes.get(ancestor);
10275
- if (node && node.instance)
10214
+ if (node?.instance)
10276
10215
  return node;
10277
10216
  ancestor = ancestor.parentElement;
10278
10217
  }
@@ -10538,7 +10477,7 @@
10538
10477
  hasSurvivor &&
10539
10478
  !newMemberIds.has(layoutId)) {
10540
10479
  if (restore && restore.parent.isConnected) {
10541
- restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
10480
+ restore.parent.insertBefore(element, restore.next?.parentNode === restore.parent
10542
10481
  ? restore.next
10543
10482
  : null);
10544
10483
  node.isPresent = false;
@@ -10743,10 +10682,6 @@
10743
10682
  * Scroll positions of scrollable ancestors and window.
10744
10683
  */
10745
10684
  this.scrollPositions = new Map();
10746
- /**
10747
- * Cleanup function for scroll listeners.
10748
- */
10749
- this.removeScrollListeners = null;
10750
10685
  this.onElementScroll = (event) => {
10751
10686
  this.handleScroll(event.target);
10752
10687
  };
@@ -10774,10 +10709,10 @@
10774
10709
  this.history.push({ ...point, timestamp: time.now() });
10775
10710
  const { onStart, onMove } = this.handlers;
10776
10711
  if (!isPanStarted) {
10777
- onStart && onStart(this.lastMoveEvent, info);
10712
+ onStart?.(this.lastMoveEvent, info);
10778
10713
  this.startEvent = this.lastMoveEvent;
10779
10714
  }
10780
- onMove && onMove(this.lastMoveEvent, info);
10715
+ onMove?.(this.lastMoveEvent, info);
10781
10716
  };
10782
10717
  this.handlePointerMove = (event, info) => {
10783
10718
  /**
@@ -10802,17 +10737,15 @@
10802
10737
  // Resume animation if dragSnapToOrigin is set OR if no drag started (user just clicked)
10803
10738
  // This ensures constraint animations continue when interrupted by a click
10804
10739
  if (this.dragSnapToOrigin || !this.startEvent) {
10805
- resumeAnimation && resumeAnimation();
10740
+ resumeAnimation?.();
10806
10741
  }
10807
10742
  if (!(this.lastMoveEvent && this.lastMoveEventInfo))
10808
10743
  return;
10809
10744
  const panInfo = getPanInfo(event.type === "pointercancel"
10810
10745
  ? this.lastMoveEventInfo
10811
10746
  : transformPoint(info, this.transformPagePoint), this.history);
10812
- if (this.startEvent && onEnd) {
10813
- onEnd(event, panInfo);
10814
- }
10815
- onSessionEnd && onSessionEnd(event, panInfo);
10747
+ this.startEvent && onEnd?.(event, panInfo);
10748
+ onSessionEnd?.(event, panInfo);
10816
10749
  };
10817
10750
  // If we have more than one touch, don't start detecting this gesture
10818
10751
  if (!isPrimaryPointer(event))
@@ -10825,8 +10758,13 @@
10825
10758
  const info = extractEventInfo(event);
10826
10759
  const initialInfo = transformPoint(info, this.transformPagePoint);
10827
10760
  const { point } = initialInfo;
10828
- const { timestamp } = frameData;
10829
- this.history = [{ ...point, timestamp }];
10761
+ /**
10762
+ * Stamp the origin with time.now(), not frameData.timestamp: when
10763
+ * the frameloop is idle, frameData.timestamp is from the last frame
10764
+ * that ran, which can be seconds old, and a fast flick from a
10765
+ * standstill would then report near-zero velocity. See #1747.
10766
+ */
10767
+ this.history = [{ ...point, timestamp: time.now() }];
10830
10768
  const { onSessionStart } = handlers;
10831
10769
  onSessionStart &&
10832
10770
  onSessionStart(event, getPanInfo(initialInfo, this.history));
@@ -10850,30 +10788,19 @@
10850
10788
  const style = getComputedStyle(current);
10851
10789
  if (overflowStyles.has(style.overflowX) ||
10852
10790
  overflowStyles.has(style.overflowY)) {
10853
- this.scrollPositions.set(current, {
10854
- x: current.scrollLeft,
10855
- y: current.scrollTop,
10856
- });
10791
+ this.scrollPositions.set(current, getScroll(current));
10857
10792
  }
10858
10793
  current = current.parentElement;
10859
10794
  }
10860
10795
  // Track window scroll
10861
- this.scrollPositions.set(window, {
10862
- x: window.scrollX,
10863
- y: window.scrollY,
10864
- });
10796
+ this.scrollPositions.set(window, getScroll(window));
10797
+ this.removeScrollListeners = pipe(
10865
10798
  // Capture listener catches element scroll events as they bubble
10866
- window.addEventListener("scroll", this.onElementScroll, {
10799
+ addDomEvent(window, "scroll", this.onElementScroll, {
10867
10800
  capture: true,
10868
- });
10801
+ }),
10869
10802
  // Direct window scroll listener (window scroll doesn't bubble)
10870
- window.addEventListener("scroll", this.onWindowScroll);
10871
- this.removeScrollListeners = () => {
10872
- window.removeEventListener("scroll", this.onElementScroll, {
10873
- capture: true,
10874
- });
10875
- window.removeEventListener("scroll", this.onWindowScroll);
10876
- };
10803
+ addDomEvent(window, "scroll", this.onWindowScroll, {}));
10877
10804
  }
10878
10805
  /**
10879
10806
  * Handle scroll compensation during drag.
@@ -10886,12 +10813,7 @@
10886
10813
  if (!initial)
10887
10814
  return;
10888
10815
  const isWindow = target === window;
10889
- const current = isWindow
10890
- ? { x: window.scrollX, y: window.scrollY }
10891
- : {
10892
- x: target.scrollLeft,
10893
- y: target.scrollTop,
10894
- };
10816
+ const current = getScroll(target);
10895
10817
  const delta = { x: current.x - initial.x, y: current.y - initial.y };
10896
10818
  if (delta.x === 0 && delta.y === 0)
10897
10819
  return;
@@ -10916,12 +10838,18 @@
10916
10838
  this.handlers = handlers;
10917
10839
  }
10918
10840
  end() {
10919
- this.removeListeners && this.removeListeners();
10920
- this.removeScrollListeners && this.removeScrollListeners();
10841
+ this.removeListeners?.();
10842
+ this.removeScrollListeners?.();
10921
10843
  this.scrollPositions.clear();
10922
10844
  cancelFrame(this.updatePoint);
10923
10845
  }
10924
10846
  }
10847
+ const getScroll = (target) => target === window
10848
+ ? { x: window.scrollX, y: window.scrollY }
10849
+ : {
10850
+ x: target.scrollLeft,
10851
+ y: target.scrollTop,
10852
+ };
10925
10853
  function transformPoint(info, transformPagePoint) {
10926
10854
  return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
10927
10855
  }
@@ -11178,6 +11106,25 @@
11178
11106
  : dragElastic[label] || 0;
11179
11107
  }
11180
11108
 
11109
+ /**
11110
+ * Re-read the scroll of the projection's ancestors, and return that of the
11111
+ * `layoutScroll` ones as a key that changes when any of them scroll.
11112
+ *
11113
+ * Clear the cached scroll first so `updateScroll` bypasses its
11114
+ * per-animationId cache and re-reads the live value.
11115
+ */
11116
+ function measureScroll(projection) {
11117
+ let key = "";
11118
+ for (const node of projection.path) {
11119
+ node.scroll = undefined;
11120
+ node.updateScroll();
11121
+ const { scroll } = node;
11122
+ if (node !== projection.root && scroll) {
11123
+ key += scroll.offset.x + "," + scroll.offset.y + ",";
11124
+ }
11125
+ }
11126
+ return key;
11127
+ }
11181
11128
  const elementDragControls = new WeakMap();
11182
11129
  class VisualElementDragControls {
11183
11130
  constructor(visualElement, readProps = () => visualElement.getProps()) {
@@ -11209,8 +11156,7 @@
11209
11156
  /**
11210
11157
  * Don't start dragging if this component is exiting
11211
11158
  */
11212
- const { presenceContext } = this.visualElement;
11213
- if (presenceContext && presenceContext.isPresent === false)
11159
+ if (this.visualElement.presenceContext?.isPresent === false)
11214
11160
  return;
11215
11161
  const onSessionStart = (event) => {
11216
11162
  if (snapToCursor)
@@ -11221,8 +11167,7 @@
11221
11167
  // Attempt to grab the global drag gesture lock - maybe make this part of PanSession
11222
11168
  const { drag, dragPropagation, onDragStart } = this.getProps();
11223
11169
  if (drag && !dragPropagation) {
11224
- if (this.openDragLock)
11225
- this.openDragLock();
11170
+ this.openDragLock?.();
11226
11171
  this.openDragLock = setDragLock(drag);
11227
11172
  // If we don 't have the lock, don't start dragging
11228
11173
  if (!this.openDragLock)
@@ -11233,9 +11178,10 @@
11233
11178
  this.isDragging = true;
11234
11179
  this.currentDirection = null;
11235
11180
  this.resolveConstraints();
11236
- if (this.visualElement.projection) {
11237
- this.visualElement.projection.isAnimationBlocked = true;
11238
- this.visualElement.projection.target = undefined;
11181
+ const { projection } = this.visualElement;
11182
+ if (projection) {
11183
+ projection.isAnimationBlocked = true;
11184
+ projection.target = undefined;
11239
11185
  }
11240
11186
  /**
11241
11187
  * Record gesture origin and pointer offset
@@ -11257,13 +11203,11 @@
11257
11203
  current = parseFloat(current);
11258
11204
  }
11259
11205
  else if (percent.test(current)) {
11260
- const { projection } = this.visualElement;
11261
- if (projection && projection.layout) {
11262
- const measuredAxis = projection.layout.layoutBox[axis];
11263
- if (measuredAxis) {
11264
- const length = calcLength(measuredAxis);
11265
- current = length * (parseFloat(current) / 100);
11266
- }
11206
+ const measuredAxis = projection?.layout?.layoutBox[axis];
11207
+ if (measuredAxis) {
11208
+ current =
11209
+ calcLength(measuredAxis) *
11210
+ (parseFloat(current) / 100);
11267
11211
  }
11268
11212
  }
11269
11213
  this.originPoint[axis] = current;
@@ -11273,8 +11217,7 @@
11273
11217
  frame.update(() => onDragStart(event, info), false, true);
11274
11218
  }
11275
11219
  addValueToWillChange(this.visualElement, "transform");
11276
- const { animationState } = this.visualElement;
11277
- animationState && animationState.setActive("whileDrag", true);
11220
+ this.visualElement.animationState?.setActive("whileDrag", true);
11278
11221
  };
11279
11222
  const onMove = (event, info) => {
11280
11223
  this.latestPointerEvent = event;
@@ -11297,8 +11240,8 @@
11297
11240
  onDirectionLock && onDirectionLock(this.currentDirection);
11298
11241
  }
11299
11242
  // Update each point with the latest position
11300
- this.updateAxis("x", info.point, offset);
11301
- this.updateAxis("y", info.point, offset);
11243
+ this.updateAxis("x", offset);
11244
+ this.updateAxis("y", offset);
11302
11245
  /**
11303
11246
  * Ideally we would leave the renderer to fire naturally at the end of
11304
11247
  * this frame but if the element is about to change layout as the result
@@ -11367,12 +11310,11 @@
11367
11310
  projection.isAnimationBlocked = false;
11368
11311
  }
11369
11312
  this.endPanSession();
11370
- const { dragPropagation } = this.getProps();
11371
- if (!dragPropagation && this.openDragLock) {
11313
+ if (!this.getProps().dragPropagation && this.openDragLock) {
11372
11314
  this.openDragLock();
11373
11315
  this.openDragLock = null;
11374
11316
  }
11375
- animationState && animationState.setActive("whileDrag", false);
11317
+ animationState?.setActive("whileDrag", false);
11376
11318
  }
11377
11319
  /**
11378
11320
  * Clean up the pan session without modifying other drag state.
@@ -11380,31 +11322,42 @@
11380
11322
  * without affecting projection animations or drag locks.
11381
11323
  */
11382
11324
  endPanSession() {
11383
- this.panSession && this.panSession.end();
11325
+ this.panSession?.end();
11384
11326
  this.panSession = undefined;
11385
11327
  }
11386
- updateAxis(axis, _point, offset) {
11387
- const { drag } = this.getProps();
11328
+ updateAxis(axis, offset) {
11329
+ /**
11330
+ * Read drag per axis: a callback fired while updating the x axis
11331
+ * can synchronously re-render with new props.
11332
+ */
11333
+ const { drag } = this.readProps();
11388
11334
  // If we're not dragging this axis, do an early return.
11389
11335
  if (!offset || !shouldDrag(axis, drag, this.currentDirection))
11390
11336
  return;
11391
11337
  const axisValue = this.getAxisMotionValue(axis);
11392
11338
  let next = this.originPoint[axis] + offset[axis];
11393
11339
  // Apply constraints
11394
- if (this.constraints && this.constraints[axis]) {
11395
- next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
11340
+ const constraints = this.constraints && this.constraints[axis];
11341
+ if (constraints) {
11342
+ next = applyConstraints(next, constraints, this.elastic[axis]);
11396
11343
  }
11397
11344
  axisValue.set(next);
11398
11345
  }
11399
11346
  resolveConstraints() {
11400
11347
  const { dragConstraints, dragElastic } = this.getProps();
11401
- const layout = this.visualElement.projection &&
11402
- !this.visualElement.projection.layout
11403
- ? this.visualElement.projection.measure(false)
11404
- : this.visualElement.projection?.layout;
11348
+ const { projection } = this.visualElement;
11349
+ const layout = projection && (projection.layout || projection.measure(false));
11405
11350
  const prevConstraints = this.constraints;
11406
11351
  if (isElementConstraints(dragConstraints)) {
11407
- if (!this.constraints) {
11352
+ /**
11353
+ * Ref constraints are measured with the layout. Scrolling a
11354
+ * `layoutScroll` ancestor since can move the element relative
11355
+ * to them without a layout measurement, so measure them again
11356
+ * (#2024). Otherwise keep them: they and the layout can both
11357
+ * have moved on the page since, e.g. when content above grows.
11358
+ */
11359
+ if (!this.constraints ||
11360
+ this.scroll !== measureScroll(projection)) {
11408
11361
  this.constraints = this.resolveRefConstraints();
11409
11362
  }
11410
11363
  }
@@ -11447,21 +11400,20 @@
11447
11400
  if (!projection || !projection.layout)
11448
11401
  return false;
11449
11402
  /**
11450
- * Refresh the root scroll offset so the constraint's viewport box
11451
- * translates to correct page coordinates. The scroll captured at
11452
- * drag mount can be stale if the document was scrolled afterwards —
11453
- * e.g. via the browser restoring scroll on refresh, or an ancestor
11454
- * layout effect running after this element's mount (#2829).
11455
- *
11456
- * Clear the cached scroll first so `updateScroll` bypasses its
11457
- * per-animationId cache and re-reads the live value.
11403
+ * Refresh the scroll offsets of the root and `layoutScroll`
11404
+ * ancestors so the constraint's viewport box translates to the
11405
+ * layout's coordinates. The scroll captured when the layout was
11406
+ * measured can be stale if they've scrolled since — e.g. via the
11407
+ * browser restoring scroll on refresh, or an ancestor layout effect
11408
+ * running after this element's mount (#2829).
11458
11409
  */
11459
- if (projection.root) {
11460
- projection.root.scroll = undefined;
11461
- projection.root.updateScroll();
11462
- }
11410
+ this.scroll = measureScroll(projection);
11463
11411
  const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
11464
- let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
11412
+ /**
11413
+ * layoutBox removes the scroll of `layoutScroll` ancestors, so
11414
+ * remove it from the constraints box too.
11415
+ */
11416
+ let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, projection.removeElementScroll(constraintsBox));
11465
11417
  /**
11466
11418
  * If there's an onMeasureDragConstraints listener we call it and
11467
11419
  * if different constraints are returned, set constraints to that
@@ -11482,7 +11434,7 @@
11482
11434
  if (!shouldDrag(axis, dragAxes || drag, this.currentDirection)) {
11483
11435
  return;
11484
11436
  }
11485
- let transition = (constraints && constraints[axis]) || {};
11437
+ let transition = constraints[axis] || {};
11486
11438
  if (dragSnapToOrigin === true ||
11487
11439
  dragSnapToOrigin === axis)
11488
11440
  transition = { min: 0, max: 0 };
@@ -11588,7 +11540,7 @@
11588
11540
  this.visualElement.current.style.transform = transformTemplate
11589
11541
  ? transformTemplate({}, "")
11590
11542
  : "none";
11591
- projection.root && projection.root.updateScroll();
11543
+ projection.root?.updateScroll();
11592
11544
  projection.updateLayout();
11593
11545
  /**
11594
11546
  * Reset constraints so resolveConstraints() will recalculate them
@@ -11665,8 +11617,8 @@
11665
11617
  };
11666
11618
  const { projection } = this.visualElement;
11667
11619
  const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
11668
- if (projection && !projection.layout) {
11669
- projection.root && projection.root.updateScroll();
11620
+ if (!projection.layout) {
11621
+ projection.root?.updateScroll();
11670
11622
  projection.updateLayout();
11671
11623
  }
11672
11624
  frame.read(measureDragConstraints);
@@ -11701,22 +11653,14 @@
11701
11653
  stopResizeListener();
11702
11654
  stopPointerListener();
11703
11655
  stopMeasureLayoutListener();
11704
- stopLayoutUpdateListener && stopLayoutUpdateListener();
11705
- stopResizeObservers && stopResizeObservers();
11656
+ stopLayoutUpdateListener();
11657
+ stopResizeObservers?.();
11706
11658
  };
11707
11659
  }
11708
11660
  getProps() {
11709
11661
  const props = this.readProps();
11710
- const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true, } = props;
11711
- return {
11712
- ...props,
11713
- drag,
11714
- dragDirectionLock,
11715
- dragPropagation,
11716
- dragConstraints,
11717
- dragElastic,
11718
- dragMomentum,
11719
- };
11662
+ const { dragElastic = defaultElastic, dragMomentum = true } = props;
11663
+ return { ...props, dragElastic, dragMomentum };
11720
11664
  }
11721
11665
  }
11722
11666
  /**
@@ -14051,6 +13995,12 @@
14051
13995
  }
14052
13996
 
14053
13997
  const reversePriorityOrder = [...variantPriorityOrder].reverse();
13998
+ /**
13999
+ * The transition a value uses when it leaves a state (#1725): the state's
14000
+ * own transition when `out: true`, or the `out` transition itself.
14001
+ * Value-specific keys are resolved by animateTarget.
14002
+ */
14003
+ const getOutTransition = (transition) => transition?.out === true ? transition : transition?.out;
14054
14004
  /**
14055
14005
  * Diffs the element's animation props (one layer per AnimationType, highest
14056
14006
  * priority wins each value) against the previous call and starts animations
@@ -14072,13 +14022,18 @@
14072
14022
  let wasReset = false;
14073
14023
  function animateChanges(changedActiveType) {
14074
14024
  const { props, parent, manuallyAnimateOnMount } = visualElement;
14025
+ const { initial } = props;
14075
14026
  const isMounting = isInitialRender || wasReset;
14076
14027
  const animations = [];
14028
+ const resolveInitial = (definition = initial) => (typeof definition !== "boolean" &&
14029
+ resolveVariant(visualElement, Array.isArray(definition) ? definition[0] : definition, visualElement.presenceContext?.custom)) ||
14030
+ undefined;
14077
14031
  /**
14078
- * Values removed from a type. A lower-priority type that defines
14079
- * them animates them back, otherwise they fall back to a base target.
14032
+ * Values removed from a type, mapped to the out transition of the
14033
+ * state they're leaving. A lower-priority type that defines them
14034
+ * animates them back, otherwise they fall back to a base target.
14080
14035
  */
14081
- const removedKeys = new Set();
14036
+ const removedKeys = new Map();
14082
14037
  /**
14083
14038
  * Values claimed by active, higher-priority types.
14084
14039
  */
@@ -14096,6 +14051,13 @@
14096
14051
  do
14097
14052
  source = source.props.inherit !== false && source.parent;
14098
14053
  while (source && !source.isControllingVariants);
14054
+ /**
14055
+ * Mounting leaves initial, so values use its out transition. Like the
14056
+ * other types, variant children inherit initial's label.
14057
+ */
14058
+ const initialOut = isMounting &&
14059
+ getOutTransition(resolveInitial(initial ?? (source && source.props.initial))
14060
+ ?.transition);
14099
14061
  reversePriorityOrder.forEach((type, i) => {
14100
14062
  const typeState = state[type];
14101
14063
  const ownProp = props[type];
@@ -14107,8 +14069,6 @@
14107
14069
  : undefined;
14108
14070
  const propIsVariant = isVariantLabel(prop);
14109
14071
  const activeDelta = type === changedActiveType ? typeState.isActive : null;
14110
- if (activeDelta === false)
14111
- removedVariantIndex = i;
14112
14072
  /**
14113
14073
  * Inherited labels are animated by the parent through
14114
14074
  * variantChildren, unless this element mounted after its parent.
@@ -14123,6 +14083,13 @@
14123
14083
  typeof prop === "boolean") {
14124
14084
  return;
14125
14085
  }
14086
+ /**
14087
+ * Set after the skip above, so deactivating a type that has nothing
14088
+ * to animate (e.g. whileTap with no prop) doesn't re-run the variants
14089
+ * below it.
14090
+ */
14091
+ if (activeDelta === false)
14092
+ removedVariantIndex = i;
14126
14093
  /**
14127
14094
  * Don't re-resolve a running exit: a changed custom would start
14128
14095
  * new value animations that stop the originals, leaving the exit
@@ -14142,6 +14109,7 @@
14142
14109
  let handledRemovedValues = false;
14143
14110
  const definitionList = Array.isArray(prop) ? prop : [prop];
14144
14111
  let resolvedValues = {};
14112
+ let out;
14145
14113
  if (activeDelta !== false) {
14146
14114
  for (const definition of definitionList) {
14147
14115
  const { transition, transitionEnd, ...target } = resolveVariant(visualElement, definition, getTypeCustom(visualElement, type)) || {};
@@ -14150,6 +14118,11 @@
14150
14118
  ...target,
14151
14119
  ...transitionEnd,
14152
14120
  };
14121
+ /**
14122
+ * One out transition per type. With a list of labels,
14123
+ * the last one that sets out applies to all its values.
14124
+ */
14125
+ out = getOutTransition(transition) || out;
14153
14126
  }
14154
14127
  }
14155
14128
  const { prevResolvedValues } = typeState;
@@ -14165,12 +14138,18 @@
14165
14138
  const valueHasChanged = Array.isArray(next) && Array.isArray(prev)
14166
14139
  ? !shallowCompare(next, prev) || variantDidChange
14167
14140
  : next !== prev;
14141
+ let outTransition;
14168
14142
  if (valueHasChanged && (next === undefined || next === null)) {
14169
- removedKeys.add(key);
14143
+ /**
14144
+ * The highest-priority state a value leaves wins.
14145
+ */
14146
+ removedKeys.has(key) ||
14147
+ removedKeys.set(key, typeState.prevOut);
14170
14148
  }
14171
14149
  else if (valueHasChanged ||
14172
14150
  (next !== undefined && removedKeys.has(key))) {
14173
14151
  shouldAnimateType = true;
14152
+ outTransition = removedKeys.get(key) || initialOut;
14174
14153
  if (removedKeys.delete(key))
14175
14154
  handledRemovedValues = true;
14176
14155
  typeState.needsAnimating[key] = true;
@@ -14181,9 +14160,11 @@
14181
14160
  else {
14182
14161
  typeState.protectedKeys[key] = true;
14183
14162
  }
14163
+ typeState.out[key] = outTransition;
14184
14164
  }
14185
14165
  typeState.prevProp = prop;
14186
14166
  typeState.prevResolvedValues = resolvedValues;
14167
+ typeState.prevOut = out;
14187
14168
  if (typeState.isActive) {
14188
14169
  encounteredKeys = { ...encounteredKeys, ...resolvedValues };
14189
14170
  }
@@ -14214,14 +14195,17 @@
14214
14195
  * longer defines a value it once did, it stays where it is.
14215
14196
  */
14216
14197
  if (removedKeys.size) {
14217
- const { initial } = props;
14218
14198
  const fallbackAnimation = {};
14219
- const resolvedInitial = typeof initial !== "boolean" &&
14220
- resolveVariant(visualElement, Array.isArray(initial) ? initial[0] : initial, visualElement.presenceContext?.custom);
14221
- if (resolvedInitial && resolvedInitial.transition) {
14222
- fallbackAnimation.transition = resolvedInitial.transition;
14223
- }
14224
- removedKeys.forEach((key) => {
14199
+ const resolvedInitial = resolveInitial();
14200
+ let fallbackTransition = resolvedInitial?.transition;
14201
+ removedKeys.forEach((outTransition, key) => {
14202
+ if (outTransition) {
14203
+ fallbackTransition = {
14204
+ ...(fallbackTransition ||
14205
+ visualElement.getDefaultTransition()),
14206
+ [key]: outTransition,
14207
+ };
14208
+ }
14225
14209
  const motionValue = visualElement.getValue(key);
14226
14210
  if (motionValue)
14227
14211
  motionValue.liveStyle = true;
@@ -14252,6 +14236,9 @@
14252
14236
  visualElement.baseTarget[key] = motionValue?.get();
14253
14237
  }
14254
14238
  });
14239
+ if (fallbackTransition) {
14240
+ fallbackAnimation.transition = fallbackTransition;
14241
+ }
14255
14242
  animations.push({ animation: fallbackAnimation });
14256
14243
  }
14257
14244
  const blockAnimation = (isMounting && visualElement.blockInitialAnimation) ||
@@ -14311,6 +14298,7 @@
14311
14298
  protectedKeys: {},
14312
14299
  needsAnimating: {},
14313
14300
  prevResolvedValues: {},
14301
+ out: {},
14314
14302
  };
14315
14303
  }
14316
14304
  return state;
@@ -14429,6 +14417,18 @@
14429
14417
  return duration * (repeat + 1) + repeatDelay * repeat;
14430
14418
  }
14431
14419
 
14420
+ /**
14421
+ * The time of a label, or a label with an offset like "label+1" or
14422
+ * "label-0.5". An exact label name wins over an offset.
14423
+ */
14424
+ function calcLabelTime(position, labels) {
14425
+ if (labels.has(position))
14426
+ return labels.get(position);
14427
+ const [, name, offset] = position.match(/^(.+?)([+-][\d.]+)$/) || [];
14428
+ return labels.has(name)
14429
+ ? Math.max(0, labels.get(name) + parseFloat(offset))
14430
+ : undefined;
14431
+ }
14432
14432
  /**
14433
14433
  * Given a absolute or relative time definition and current/prev time state of the sequence,
14434
14434
  * calculate an absolute time for the next keyframes.
@@ -14447,7 +14447,7 @@
14447
14447
  return Math.max(0, prev + parseFloat(next.slice(1)));
14448
14448
  }
14449
14449
  else {
14450
- return labels.get(next) ?? current;
14450
+ return calcLabelTime(next, labels) ?? current;
14451
14451
  }
14452
14452
  }
14453
14453
 
@@ -14508,7 +14508,7 @@
14508
14508
 
14509
14509
  const defaultSegmentEasing = "easeInOut";
14510
14510
  const MAX_REPEAT = 20;
14511
- function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
14511
+ function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators, info) {
14512
14512
  const defaultDuration = defaultTransition.duration || 0.3;
14513
14513
  const animationDefinitions = new Map();
14514
14514
  const sequences = new Map();
@@ -14521,196 +14521,233 @@
14521
14521
  * Build the timeline by mapping over the sequence array and converting
14522
14522
  * the definitions into keyframes and offsets with absolute time values.
14523
14523
  * These will later get converted into relative offsets in a second pass.
14524
+ *
14525
+ * Nested sequences are resolved recursively. `base` is the absolute
14526
+ * start of the sequence being resolved, so numeric `at` values inside
14527
+ * a nested sequence are relative to where it starts. Returns the time
14528
+ * the sequence ends.
14524
14529
  */
14525
- for (let i = 0; i < sequence.length; i++) {
14526
- const segment = sequence[i];
14527
- /**
14528
- * If this is a timeline label, mark it and skip the rest of this iteration.
14529
- */
14530
- if (typeof segment === "string") {
14531
- timeLabels.set(segment, currentTime);
14532
- continue;
14533
- }
14534
- else if (!Array.isArray(segment)) {
14535
- timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
14536
- continue;
14537
- }
14538
- let [subject, keyframes, transition = {}] = segment;
14539
- /**
14540
- * If a relative or absolute time value has been specified we need to resolve
14541
- * it in relation to the currentTime.
14542
- */
14543
- if (transition.at !== undefined) {
14544
- currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
14545
- }
14546
- /**
14547
- * Keep track of the maximum duration in this definition. This will be
14548
- * applied to currentTime once the definition has been parsed.
14549
- */
14550
- let maxDuration = 0;
14551
- const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
14552
- const valueKeyframesAsList = keyframesAsList(valueKeyframes);
14553
- const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
14554
- let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
14530
+ const resolveSequence = (sequence, base) => {
14531
+ let sequenceEnd = base;
14532
+ const resolveTime = (at) => typeof at === "number"
14533
+ ? base + at
14534
+ : calcNextTime(currentTime, at, prevTime, timeLabels);
14535
+ for (let i = 0; i < sequence.length; i++) {
14536
+ const segment = sequence[i];
14555
14537
  /**
14556
- * Resolve stagger() if defined.
14538
+ * If this is a timeline label, mark it and skip the rest of this iteration.
14557
14539
  */
14558
- const calculatedDelay = typeof delay === "function"
14559
- ? delay(elementIndex, numSubjects)
14560
- : delay;
14561
- /**
14562
- * If this animation should and can use a spring, generate a spring easing function.
14563
- */
14564
- const numKeyframes = valueKeyframesAsList.length;
14565
- const createGenerator = isGenerator(type)
14566
- ? type
14567
- : generators?.[type || "keyframes"];
14568
- if (numKeyframes <= 2 && createGenerator) {
14569
- /**
14570
- * As we're creating an easing function from a spring,
14571
- * ideally we want to generate it using the real distance
14572
- * between the two keyframes. However this isn't always
14573
- * possible - in these situations we use 0-100.
14574
- */
14575
- let absoluteDelta = 100;
14576
- if (numKeyframes === 2 &&
14577
- isNumberKeyframesArray(valueKeyframesAsList)) {
14578
- const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
14579
- absoluteDelta = Math.abs(delta);
14540
+ if (typeof segment === "string") {
14541
+ timeLabels.set(segment, currentTime);
14542
+ continue;
14543
+ }
14544
+ else if (!Array.isArray(segment)) {
14545
+ const start = segment.at === undefined
14546
+ ? currentTime
14547
+ : resolveTime(segment.at);
14548
+ if ("sequence" in segment) {
14549
+ /**
14550
+ * Resolve the nested sequence from its start. Afterwards
14551
+ * "<" points at its start and the next segment follows
14552
+ * its end, as if it were a single segment.
14553
+ */
14554
+ prevTime = currentTime = start;
14555
+ currentTime = resolveSequence(segment.sequence, start);
14556
+ prevTime = start;
14557
+ sequenceEnd = Math.max(sequenceEnd, currentTime);
14580
14558
  }
14581
- const springTransition = {
14582
- ...defaultTransition,
14583
- ...remainingTransition,
14584
- };
14585
- if (duration !== undefined) {
14586
- springTransition.duration = secondsToMilliseconds(duration);
14559
+ else {
14560
+ timeLabels.set(segment.name, start);
14587
14561
  }
14588
- const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
14589
- ease = springEasing.ease;
14590
- duration = springEasing.duration;
14562
+ continue;
14591
14563
  }
14592
- duration ?? (duration = defaultDuration);
14593
- const startTime = currentTime + calculatedDelay;
14564
+ let [subject, keyframes, transition = {}] = segment;
14594
14565
  /**
14595
- * If there's only one time offset of 0, fill in a second with length 1
14566
+ * If a relative or absolute time value has been specified we need to resolve
14567
+ * it in relation to the currentTime.
14596
14568
  */
14597
- if (times.length === 1 && times[0] === 0) {
14598
- times[1] = 1;
14569
+ if (transition.at !== undefined) {
14570
+ currentTime = resolveTime(transition.at);
14599
14571
  }
14600
14572
  /**
14601
- * Fill out if offset if fewer offsets than keyframes
14602
- */
14603
- const remainder = times.length - valueKeyframesAsList.length;
14604
- remainder > 0 && fillOffset(times, remainder);
14605
- /**
14606
- * If only one value has been set, ie [1], push a null to the start of
14607
- * the keyframe array. This will let us mark a keyframe at this point
14608
- * that will later be hydrated with the previous value.
14609
- */
14610
- valueKeyframesAsList.length === 1 &&
14611
- valueKeyframesAsList.unshift(null);
14612
- /**
14613
- * Segments can't express `repeat: Infinity` or very large
14614
- * counts — they'd leave dead time after the segment or
14615
- * explode the keyframe array. Ignore with a warning.
14573
+ * Keep track of the maximum duration in this definition. This will be
14574
+ * applied to currentTime once the definition has been parsed.
14616
14575
  */
14617
- if (repeat) {
14618
- exports.warning(repeat < MAX_REPEAT, `Sequence segments can't repeat ${repeat} times — ignoring repeat option. Use a value below ${MAX_REPEAT} or apply repeat at the sequence level instead.`);
14619
- }
14620
- if (repeat && repeat < MAX_REPEAT) {
14576
+ let maxDuration = 0;
14577
+ const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
14578
+ const valueKeyframesAsList = keyframesAsList(valueKeyframes);
14579
+ const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
14580
+ let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
14621
14581
  /**
14622
- * Express repeatDelay in units of a single iteration's duration
14623
- * so it can be added to the per-iteration time offsets below
14624
- * before they're normalized to 0-1.
14582
+ * Resolve stagger() if defined.
14625
14583
  */
14626
- const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
14627
- duration = calculateRepeatDuration(duration, repeat, repeatDelay);
14628
- const originalKeyframes = [...valueKeyframesAsList];
14629
- const originalTimes = [...times];
14630
- ease = Array.isArray(ease) ? [...ease] : [ease];
14631
- const originalEase = [...ease];
14584
+ const calculatedDelay = typeof delay === "function"
14585
+ ? delay(elementIndex, numSubjects)
14586
+ : delay;
14632
14587
  /**
14633
- * For reverse/mirror, alternate iterations play the segment
14634
- * backwards. mirror matches JSAnimation's mirroredGenerator:
14635
- * reversed keyframes, easings unchanged. reverse matches
14636
- * JSAnimation's iterationProgress = 1 - p: reversed
14637
- * keyframes, easing array reversed AND each function easing
14638
- * mapped through reverseEasing (string easings unchanged —
14639
- * they're resolved later by the keyframes engine).
14588
+ * If this animation should and can use a spring, generate a spring easing function.
14640
14589
  */
14641
- const isFlipping = repeatType === "reverse" || repeatType === "mirror";
14642
- let flippedKeyframes = originalKeyframes;
14643
- let flippedEases = originalEase;
14644
- if (isFlipping) {
14645
- flippedKeyframes = [...originalKeyframes].reverse();
14646
- if (repeatType === "reverse") {
14647
- flippedEases = [...originalEase]
14648
- .reverse()
14649
- .map((e) => typeof e === "function"
14650
- ? reverseEasing(e)
14651
- : e);
14590
+ const numKeyframes = valueKeyframesAsList.length;
14591
+ const createGenerator = isGenerator(type)
14592
+ ? type
14593
+ : generators?.[type || "keyframes"];
14594
+ if (numKeyframes <= 2 && createGenerator) {
14595
+ /**
14596
+ * As we're creating an easing function from a spring,
14597
+ * ideally we want to generate it using the real distance
14598
+ * between the two keyframes. However this isn't always
14599
+ * possible - in these situations we use 0-100.
14600
+ */
14601
+ let absoluteDelta = 100;
14602
+ if (numKeyframes === 2 &&
14603
+ isNumberKeyframesArray(valueKeyframesAsList)) {
14604
+ const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
14605
+ absoluteDelta = Math.abs(delta);
14606
+ }
14607
+ const springTransition = {
14608
+ ...defaultTransition,
14609
+ ...remainingTransition,
14610
+ };
14611
+ if (duration !== undefined) {
14612
+ springTransition.duration =
14613
+ secondsToMilliseconds(duration);
14652
14614
  }
14615
+ const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
14616
+ ease = springEasing.ease;
14617
+ duration = springEasing.duration;
14618
+ }
14619
+ duration ?? (duration = defaultDuration);
14620
+ const startTime = currentTime + calculatedDelay;
14621
+ /**
14622
+ * If there's only one time offset of 0, fill in a second with length 1
14623
+ */
14624
+ if (times.length === 1 && times[0] === 0) {
14625
+ times[1] = 1;
14653
14626
  }
14654
- for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
14655
- const isFlipped = isFlipping && repeatIndex % 2 === 0;
14656
- const iterKeyframes = isFlipped
14657
- ? flippedKeyframes
14658
- : originalKeyframes;
14659
- const iterEase = isFlipped ? flippedEases : originalEase;
14660
- const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
14627
+ /**
14628
+ * Fill out if offset if fewer offsets than keyframes
14629
+ */
14630
+ const remainder = times.length - valueKeyframesAsList.length;
14631
+ remainder > 0 && fillOffset(times, remainder);
14632
+ /**
14633
+ * If only one value has been set, ie [1], push a null to the start of
14634
+ * the keyframe array. This will let us mark a keyframe at this point
14635
+ * that will later be hydrated with the previous value.
14636
+ */
14637
+ valueKeyframesAsList.length === 1 &&
14638
+ valueKeyframesAsList.unshift(null);
14639
+ /**
14640
+ * Segments can't express `repeat: Infinity` or very large
14641
+ * counts — they'd leave dead time after the segment or
14642
+ * explode the keyframe array. Ignore with a warning.
14643
+ */
14644
+ if (repeat) {
14645
+ typeof process !== "undefined" &&
14646
+ "development" !== "production" &&
14647
+ exports.warning(repeat < MAX_REPEAT, `Sequence segments can't repeat ${repeat} times — ignoring repeat option. Use a value below ${MAX_REPEAT} or apply repeat at the sequence level instead.`);
14648
+ }
14649
+ if (repeat && repeat < MAX_REPEAT) {
14661
14650
  /**
14662
- * If repeatDelay is set, hold the previous iteration's
14663
- * final value through the delay by inserting a keyframe
14664
- * at the moment the next iteration begins.
14651
+ * Express repeatDelay in units of a single iteration's duration
14652
+ * so it can be added to the per-iteration time offsets below
14653
+ * before they're normalized to 0-1.
14665
14654
  */
14666
- if (repeatDelayUnits > 0) {
14667
- valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
14668
- times.push(iterStartOffset);
14669
- ease.push("linear");
14655
+ const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
14656
+ duration = calculateRepeatDuration(duration, repeat, repeatDelay);
14657
+ const originalKeyframes = [...valueKeyframesAsList];
14658
+ const originalTimes = [...times];
14659
+ ease = Array.isArray(ease) ? [...ease] : [ease];
14660
+ const originalEase = [...ease];
14661
+ /**
14662
+ * For reverse/mirror, alternate iterations play the segment
14663
+ * backwards. mirror matches JSAnimation's mirroredGenerator:
14664
+ * reversed keyframes, easings unchanged. reverse matches
14665
+ * JSAnimation's iterationProgress = 1 - p: reversed
14666
+ * keyframes, easing array reversed AND each function easing
14667
+ * mapped through reverseEasing (string easings unchanged —
14668
+ * they're resolved later by the keyframes engine).
14669
+ */
14670
+ const isFlipping = repeatType === "reverse" || repeatType === "mirror";
14671
+ let flippedKeyframes = originalKeyframes;
14672
+ let flippedEases = originalEase;
14673
+ if (isFlipping) {
14674
+ flippedKeyframes = [...originalKeyframes].reverse();
14675
+ if (repeatType === "reverse") {
14676
+ flippedEases = [...originalEase]
14677
+ .reverse()
14678
+ .map((e) => typeof e === "function"
14679
+ ? reverseEasing(e)
14680
+ : e);
14681
+ }
14670
14682
  }
14671
- valueKeyframesAsList.push(...iterKeyframes);
14672
- for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
14673
- times.push(originalTimes[keyframeIndex] + iterStartOffset);
14674
- ease.push(keyframeIndex === 0
14675
- ? "linear"
14676
- : getEasingForSegment(iterEase, keyframeIndex - 1));
14683
+ for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
14684
+ const isFlipped = isFlipping && repeatIndex % 2 === 0;
14685
+ const iterKeyframes = isFlipped
14686
+ ? flippedKeyframes
14687
+ : originalKeyframes;
14688
+ const iterEase = isFlipped ? flippedEases : originalEase;
14689
+ const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
14690
+ /**
14691
+ * If repeatDelay is set, hold the previous iteration's
14692
+ * final value through the delay by inserting a keyframe
14693
+ * at the moment the next iteration begins.
14694
+ */
14695
+ if (repeatDelayUnits > 0) {
14696
+ valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
14697
+ times.push(iterStartOffset);
14698
+ ease.push("linear");
14699
+ }
14700
+ valueKeyframesAsList.push(...iterKeyframes);
14701
+ for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
14702
+ times.push(originalTimes[keyframeIndex] + iterStartOffset);
14703
+ ease.push(keyframeIndex === 0
14704
+ ? "linear"
14705
+ : getEasingForSegment(iterEase, keyframeIndex - 1));
14706
+ }
14677
14707
  }
14708
+ normalizeTimes(times, repeat, repeatDelayUnits);
14678
14709
  }
14679
- normalizeTimes(times, repeat, repeatDelayUnits);
14710
+ const targetTime = startTime + duration;
14711
+ /**
14712
+ * Add keyframes, mapping offsets to absolute time.
14713
+ */
14714
+ addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
14715
+ maxDuration = Math.max(calculatedDelay + duration, maxDuration);
14716
+ totalDuration = Math.max(targetTime, totalDuration);
14717
+ };
14718
+ if (isMotionValue(subject)) {
14719
+ const subjectSequence = getSubjectSequence(subject, sequences);
14720
+ resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
14680
14721
  }
14681
- const targetTime = startTime + duration;
14682
- /**
14683
- * Add keyframes, mapping offsets to absolute time.
14684
- */
14685
- addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
14686
- maxDuration = Math.max(calculatedDelay + duration, maxDuration);
14687
- totalDuration = Math.max(targetTime, totalDuration);
14688
- };
14689
- if (isMotionValue(subject)) {
14690
- const subjectSequence = getSubjectSequence(subject, sequences);
14691
- resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
14692
- }
14693
- else {
14694
- const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
14695
- const numSubjects = subjects.length;
14696
- /**
14697
- * For every element in this segment, process the defined values.
14698
- */
14699
- for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
14722
+ else {
14723
+ const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
14724
+ const numSubjects = subjects.length;
14700
14725
  /**
14701
- * Cast necessary, but we know these are of this type
14726
+ * For every element in this segment, process the defined values.
14702
14727
  */
14703
- keyframes = keyframes;
14704
- transition = transition;
14705
- const thisSubject = subjects[subjectIndex];
14706
- const subjectSequence = getSubjectSequence(thisSubject, sequences);
14707
- for (const key in keyframes) {
14708
- resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
14728
+ for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
14729
+ /**
14730
+ * Cast necessary, but we know these are of this type
14731
+ */
14732
+ keyframes = keyframes;
14733
+ transition = transition;
14734
+ const thisSubject = subjects[subjectIndex];
14735
+ const subjectSequence = getSubjectSequence(thisSubject, sequences);
14736
+ for (const key in keyframes) {
14737
+ resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
14738
+ }
14709
14739
  }
14710
14740
  }
14741
+ prevTime = currentTime;
14742
+ currentTime += maxDuration;
14743
+ sequenceEnd = Math.max(sequenceEnd, currentTime);
14711
14744
  }
14712
- prevTime = currentTime;
14713
- currentTime += maxDuration;
14745
+ return sequenceEnd;
14746
+ };
14747
+ resolveSequence(sequence, 0);
14748
+ if (info) {
14749
+ info.duration = totalDuration;
14750
+ info.labels = timeLabels;
14714
14751
  }
14715
14752
  /**
14716
14753
  * For every element and value combination create a new animation.
@@ -14858,7 +14895,7 @@
14858
14895
  return animations;
14859
14896
  }
14860
14897
 
14861
- function animateSequence(sequence, options, scope) {
14898
+ function animateSequence(sequence, options, scope, info) {
14862
14899
  const animations = [];
14863
14900
  /**
14864
14901
  * Pre-process: replace function segments with MotionValue segments,
@@ -14881,13 +14918,515 @@
14881
14918
  }
14882
14919
  return segment;
14883
14920
  });
14884
- const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring });
14921
+ const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring }, info);
14885
14922
  animationDefinitions.forEach(({ keyframes, transition }, subject) => {
14886
14923
  animations.push(...animateSubject(subject, keyframes, transition));
14887
14924
  });
14888
14925
  return animations;
14889
14926
  }
14890
14927
 
14928
+ let markerId = 0;
14929
+ /**
14930
+ * Copy a segment with a new `at`, adding a transition if it has none.
14931
+ */
14932
+ function withAt(segment, at) {
14933
+ const copy = [...segment];
14934
+ const i = typeof copy[0] === "function" ? Math.max(1, copy.length - 1) : 2;
14935
+ copy[i] = { ...copy[i], at };
14936
+ return copy;
14937
+ }
14938
+ const label = (name, at) => at === undefined ? name : { name, at };
14939
+ /**
14940
+ * Rewind to the start and stop, so every value is back at the value it
14941
+ * started from and the next build reads those values as its origin.
14942
+ */
14943
+ function rewind(animation) {
14944
+ animation.pause();
14945
+ animation.time = 0;
14946
+ animation.stop();
14947
+ }
14948
+ /**
14949
+ * A sequence that can be changed after it's created. Segments, labels,
14950
+ * callbacks, pauses and other timelines can be added, moved and removed
14951
+ * at any point, including while it plays.
14952
+ *
14953
+ * Changes are batched and applied once per microtask. Each change
14954
+ * rebuilds the underlying animations from the timeline's starting
14955
+ * values, then restores the playhead, speed and play state, so changes
14956
+ * made while playing take effect without a jump.
14957
+ */
14958
+ class Timeline {
14959
+ /**
14960
+ * `animations` are already running animations of `items`, as
14961
+ * animate() makes them. The timeline plays these until it's first
14962
+ * changed, so it costs nothing over the animations themselves until
14963
+ * then.
14964
+ */
14965
+ constructor(items = [], options = {}, animations) {
14966
+ this.options = options;
14967
+ this.entries = [];
14968
+ /**
14969
+ * The repeat the playhead is in, to detect when it starts another.
14970
+ */
14971
+ this.repeatIndex = 0;
14972
+ this.prevTime = 0;
14973
+ this.markers = [];
14974
+ this.labels = new Map();
14975
+ this.totalDuration = 0;
14976
+ this.isDirty = false;
14977
+ this.isDone = false;
14978
+ this.isRebuilding = false;
14979
+ this.playbackSpeed = 1;
14980
+ /**
14981
+ * True while this plays the animations it was created with, before
14982
+ * any change compiles it as a sequence.
14983
+ */
14984
+ this.isWrapped = false;
14985
+ this.stop = () => {
14986
+ this.flush();
14987
+ this.group?.stop();
14988
+ };
14989
+ this.isPaused = options.autoplay === false;
14990
+ this.newFinished();
14991
+ items.forEach((item) => this.add(item));
14992
+ if (animations) {
14993
+ this.isDirty = false;
14994
+ this.isWrapped = true;
14995
+ this.group = new GroupAnimationWithThen(animations);
14996
+ this.watch();
14997
+ }
14998
+ else {
14999
+ /**
15000
+ * Build now, like animate(), so selectors resolve while a
15001
+ * scope is attached. Later changes are batched.
15002
+ */
15003
+ this.invalidate();
15004
+ this.flush();
15005
+ }
15006
+ /**
15007
+ * Register with the scope (for instance from useAnimate()) so it
15008
+ * stops when the scope is torn down.
15009
+ */
15010
+ const { scope } = options;
15011
+ if (scope) {
15012
+ scope.animations.push(this);
15013
+ this.finished.then(() => removeItem(scope.animations, this));
15014
+ }
15015
+ }
15016
+ /**
15017
+ * Add a segment, label or timeline. `at` overrides any `at` already
15018
+ * on the segment. The item itself is the handle for `move()` and
15019
+ * `remove()`.
15020
+ */
15021
+ add(item, at) {
15022
+ if (item instanceof Timeline)
15023
+ item.adopt(this);
15024
+ return this.push({ item, at });
15025
+ }
15026
+ /**
15027
+ * Fire a callback when the playhead crosses `at`, in either direction.
15028
+ * It receives the direction: 1 forwards, -1 backwards.
15029
+ */
15030
+ call(callback, at) {
15031
+ return this.push({
15032
+ item: callback,
15033
+ at,
15034
+ marker: { name: "_m" + markerId++, time: 0, callback },
15035
+ });
15036
+ }
15037
+ /**
15038
+ * Pause when the playhead reaches `at`.
15039
+ */
15040
+ addPause(at, callback) {
15041
+ return this.push({
15042
+ item: callback || {},
15043
+ at,
15044
+ marker: { name: "_m" + markerId++, time: 0, callback, pause: true },
15045
+ });
15046
+ }
15047
+ /**
15048
+ * Change where an item starts.
15049
+ */
15050
+ move(item, at) {
15051
+ this.entries.forEach((entry) => {
15052
+ if (entry.item === item)
15053
+ entry.at = at;
15054
+ });
15055
+ return this.invalidate();
15056
+ }
15057
+ /**
15058
+ * Remove a segment, label, callback or timeline.
15059
+ */
15060
+ remove(item) {
15061
+ if (item instanceof Timeline)
15062
+ item.parent = undefined;
15063
+ this.entries = this.entries.filter((entry) => entry.item !== item);
15064
+ return this.invalidate();
15065
+ }
15066
+ clear() {
15067
+ this.entries = [];
15068
+ return this.invalidate();
15069
+ }
15070
+ /**
15071
+ * The time of a label in seconds, or `undefined` if there's no label
15072
+ * with this name.
15073
+ */
15074
+ getLabelTime(name) {
15075
+ this.flush();
15076
+ return this.labels.get(name);
15077
+ }
15078
+ /**
15079
+ * Jump to a time in seconds or a label.
15080
+ */
15081
+ seek(position) {
15082
+ this.time = this.resolve(position);
15083
+ return this;
15084
+ }
15085
+ /**
15086
+ * Play forwards or backwards to a time or label, then pause there.
15087
+ */
15088
+ playTo(position) {
15089
+ const target = this.resolve(position);
15090
+ const speed = Math.abs(this.speed);
15091
+ this.speed = target < this.time ? -speed : speed;
15092
+ this.stopAt = target;
15093
+ this.clock || this.invalidate();
15094
+ this.play();
15095
+ return this;
15096
+ }
15097
+ reverse() {
15098
+ this.speed = -this.speed;
15099
+ this.play();
15100
+ return this;
15101
+ }
15102
+ get progress() {
15103
+ return this.duration ? this.time / this.duration : 0;
15104
+ }
15105
+ set progress(progress) {
15106
+ this.time = progress * this.duration;
15107
+ }
15108
+ get time() {
15109
+ return this.active()?.time ?? 0;
15110
+ }
15111
+ set time(time) {
15112
+ this.flush();
15113
+ this.prevTime = time;
15114
+ this.group && (this.group.time = time);
15115
+ this.repeatIndex = this.currentRepeat();
15116
+ }
15117
+ get speed() {
15118
+ return this.playbackSpeed;
15119
+ }
15120
+ set speed(speed) {
15121
+ this.playbackSpeed = speed;
15122
+ this.group && (this.group.speed = speed);
15123
+ }
15124
+ get duration() {
15125
+ this.flush();
15126
+ return this.isWrapped ? this.group.duration : this.totalDuration;
15127
+ }
15128
+ get iterationDuration() {
15129
+ this.flush();
15130
+ return this.isWrapped
15131
+ ? this.group.iterationDuration
15132
+ : this.totalDuration;
15133
+ }
15134
+ get state() {
15135
+ return this.active()?.state ?? "idle";
15136
+ }
15137
+ get startTime() {
15138
+ return this.active()?.startTime ?? null;
15139
+ }
15140
+ play() {
15141
+ this.flush();
15142
+ this.isPaused = false;
15143
+ this.group?.play();
15144
+ /**
15145
+ * Watch after playing, so it waits for the restarted animations
15146
+ * rather than the ones that already finished.
15147
+ */
15148
+ if (this.isDone) {
15149
+ this.newFinished();
15150
+ this.watch();
15151
+ }
15152
+ }
15153
+ pause() {
15154
+ this.flush();
15155
+ this.isPaused = true;
15156
+ this.group?.pause();
15157
+ }
15158
+ cancel() {
15159
+ this.flush();
15160
+ this.group?.cancel();
15161
+ }
15162
+ complete() {
15163
+ this.flush();
15164
+ this.group?.complete();
15165
+ }
15166
+ attachTimeline(timeline) {
15167
+ this.flush();
15168
+ this.scrollTimeline = timeline;
15169
+ this.detachScroll = this.group?.attachTimeline(timeline);
15170
+ return () => {
15171
+ this.scrollTimeline = undefined;
15172
+ this.detachScroll?.();
15173
+ };
15174
+ }
15175
+ then(onResolve, onReject) {
15176
+ return this.finished.then(onResolve, onReject);
15177
+ }
15178
+ /**
15179
+ * The current animations, if any are running. Skipped animations
15180
+ * (skipAnimations, reduced motion) can leave the group empty.
15181
+ */
15182
+ active() {
15183
+ this.flush();
15184
+ return this.group?.animations.length ? this.group : undefined;
15185
+ }
15186
+ push(entry) {
15187
+ this.entries.push(entry);
15188
+ return this.invalidate();
15189
+ }
15190
+ /**
15191
+ * Schedule a rebuild. A nested timeline passes changes up, as it's
15192
+ * built as part of its parent.
15193
+ */
15194
+ invalidate() {
15195
+ if (this.parent) {
15196
+ this.parent.invalidate();
15197
+ }
15198
+ else if (!this.isDirty) {
15199
+ this.isDirty = true;
15200
+ queueMicrotask(() => this.flush());
15201
+ }
15202
+ return this;
15203
+ }
15204
+ /**
15205
+ * Apply pending changes now, so reads straight after a change
15206
+ * reflect it.
15207
+ */
15208
+ flush() {
15209
+ this.isDirty && !this.parent && this.build();
15210
+ }
15211
+ adopt(parent) {
15212
+ if (this.group) {
15213
+ rewind(this.group);
15214
+ this.group = undefined;
15215
+ }
15216
+ this.isDirty = false;
15217
+ this.parent = parent;
15218
+ }
15219
+ build() {
15220
+ this.isDirty = false;
15221
+ this.isWrapped = false;
15222
+ const prev = this.group;
15223
+ let time = 0;
15224
+ if (prev) {
15225
+ this.isRebuilding = true;
15226
+ if (this.scrollTimeline) {
15227
+ /**
15228
+ * Scroll-linked animations can't be seeked in seconds.
15229
+ * Cancelling also returns them to their starting values,
15230
+ * and the new build is attached to scroll instead.
15231
+ */
15232
+ this.detachScroll?.();
15233
+ prev.cancel();
15234
+ }
15235
+ else {
15236
+ time = prev.animations.length ? prev.time : 0;
15237
+ rewind(prev);
15238
+ }
15239
+ this.isRebuilding = false;
15240
+ }
15241
+ const { defaultTransition, onComplete, onUpdate, onRepeat, onReverseComplete, autoplay, scope, ...options } = this.options;
15242
+ /**
15243
+ * Inherit MotionConfig settings from a useAnimate() scope.
15244
+ */
15245
+ const playback = {};
15246
+ if (scope?.reduceMotion !== undefined) {
15247
+ playback.reduceMotion = scope.reduceMotion;
15248
+ }
15249
+ if (scope?.skipAnimations !== undefined) {
15250
+ playback.skipAnimations = scope.skipAnimations;
15251
+ }
15252
+ Object.assign(playback, options);
15253
+ const markers = new Map();
15254
+ const info = {};
15255
+ const animations = animateSequence(this.toSequence(markers), { defaultTransition, ...playback }, scope, info);
15256
+ const labels = info.labels;
15257
+ let duration = playback.duration ?? info.duration;
15258
+ /**
15259
+ * A `duration` option stretches every animation to fit, so scale
15260
+ * label and marker times to match.
15261
+ */
15262
+ const scale = info.duration ? duration / info.duration : 1;
15263
+ labels.forEach((time, name) => labels.set(name, time * scale));
15264
+ /**
15265
+ * Markers are resolved as labels. Move them out of the label map
15266
+ * and extend the duration to reach any marker past the end.
15267
+ */
15268
+ this.markers = [];
15269
+ markers.forEach((marker, name) => {
15270
+ marker.time = labels.get(name);
15271
+ labels.delete(name);
15272
+ duration = Math.max(duration, marker.time);
15273
+ this.markers.push(marker);
15274
+ });
15275
+ this.markers.sort((a, b) => a.time - b.time);
15276
+ this.labels = labels;
15277
+ this.totalDuration = duration;
15278
+ /**
15279
+ * Callbacks, pauses and playTo() watch a linear clock. Without
15280
+ * them, the timeline is only its animations.
15281
+ */
15282
+ this.clock = this.clockAnimation = undefined;
15283
+ if (this.markers.length ||
15284
+ this.stopAt !== undefined ||
15285
+ onUpdate ||
15286
+ onRepeat) {
15287
+ const clock = (this.clock = motionValue(0));
15288
+ clock.on("change", (t) => clock === this.clock && this.onClock(t));
15289
+ animations.unshift((this.clockAnimation = animateSingleValue(clock, [0, duration], {
15290
+ ...playback,
15291
+ duration,
15292
+ ease: "linear",
15293
+ })));
15294
+ }
15295
+ const group = (this.group = new GroupAnimationWithThen(animations));
15296
+ time = Math.min(time, duration);
15297
+ if (this.isDone && time < duration)
15298
+ this.newFinished();
15299
+ if (this.scrollTimeline) {
15300
+ this.detachScroll = group.attachTimeline(this.scrollTimeline);
15301
+ }
15302
+ else {
15303
+ group.speed = this.playbackSpeed;
15304
+ if (this.isPaused)
15305
+ group.pause();
15306
+ if (time)
15307
+ group.time = time;
15308
+ }
15309
+ this.repeatIndex = this.currentRepeat();
15310
+ /**
15311
+ * Treat a start from 0 as coming from just before it, so markers
15312
+ * at 0 fire.
15313
+ */
15314
+ this.prevTime = time || -1;
15315
+ this.watch();
15316
+ }
15317
+ toSequence(markers) {
15318
+ return this.entries.map(({ item, at, marker }) => {
15319
+ if (marker) {
15320
+ markers.set(marker.name, marker);
15321
+ return label(marker.name, at);
15322
+ }
15323
+ if (item instanceof Timeline) {
15324
+ return { sequence: item.toSequence(markers), at };
15325
+ }
15326
+ if (typeof item === "string")
15327
+ return label(item, at);
15328
+ if (at === undefined)
15329
+ return item;
15330
+ return Array.isArray(item)
15331
+ ? withAt(item, at)
15332
+ : { ...item, at };
15333
+ });
15334
+ }
15335
+ resolve(position) {
15336
+ this.flush();
15337
+ return typeof position === "number"
15338
+ ? position
15339
+ : calcLabelTime(position, this.labels) ?? this.time;
15340
+ }
15341
+ /**
15342
+ * The repeat the clock is in, from its time across all repeats.
15343
+ */
15344
+ currentRepeat() {
15345
+ const { repeat = 0, repeatDelay = 0, delay = 0 } = this.options;
15346
+ const cycle = this.totalDuration + repeatDelay;
15347
+ const time = (this.clockAnimation?.time ?? 0) - delay;
15348
+ return cycle && time > 0
15349
+ ? Math.min(repeat, Math.floor(time / cycle))
15350
+ : 0;
15351
+ }
15352
+ onClock(time) {
15353
+ let prevTime = this.prevTime;
15354
+ this.prevTime = time;
15355
+ if (this.isRebuilding)
15356
+ return;
15357
+ const { onUpdate, onRepeat, repeatType = "loop" } = this.options;
15358
+ onUpdate?.(time);
15359
+ const repeatIndex = this.currentRepeat();
15360
+ if (repeatIndex !== this.repeatIndex) {
15361
+ this.repeatIndex = repeatIndex;
15362
+ onRepeat?.();
15363
+ /**
15364
+ * A loop jumps the playhead back to the start, or to the
15365
+ * end when playing backwards. Finish crossing the repeat
15366
+ * that ended, then cross the new one from its edge.
15367
+ */
15368
+ if (repeatType === "loop") {
15369
+ const isForward = this.speed > 0;
15370
+ if (this.cross(prevTime, isForward ? this.totalDuration : 0)) {
15371
+ return;
15372
+ }
15373
+ prevTime = isForward ? -1 : Infinity;
15374
+ }
15375
+ }
15376
+ this.cross(prevTime, time);
15377
+ }
15378
+ /**
15379
+ * Fire markers between two playhead times. Returns `true` if it
15380
+ * paused.
15381
+ */
15382
+ cross(from, to) {
15383
+ if (from === to)
15384
+ return;
15385
+ const isForward = to > from;
15386
+ const crossed = (t) => isForward ? t > from && t <= to : t < from && t >= to;
15387
+ const markers = isForward ? this.markers : [...this.markers].reverse();
15388
+ for (const marker of markers) {
15389
+ if (!crossed(marker.time))
15390
+ continue;
15391
+ marker.callback?.(isForward ? 1 : -1);
15392
+ if (marker.pause)
15393
+ return this.pauseAt(marker.time);
15394
+ }
15395
+ if (this.stopAt !== undefined && crossed(this.stopAt)) {
15396
+ this.pauseAt(this.stopAt);
15397
+ this.stopAt = undefined;
15398
+ return true;
15399
+ }
15400
+ }
15401
+ pauseAt(time) {
15402
+ this.pause();
15403
+ this.time = time;
15404
+ return true;
15405
+ }
15406
+ watch() {
15407
+ const group = this.group;
15408
+ group.finished.then(() => {
15409
+ if (group !== this.group || this.isDone)
15410
+ return;
15411
+ this.isDone = true;
15412
+ const { onComplete, onReverseComplete } = this.options;
15413
+ /**
15414
+ * Without onReverseComplete, onComplete fires in either
15415
+ * direction, as it does for a single animation.
15416
+ */
15417
+ const callback = (this.speed < 0 && onReverseComplete) || onComplete;
15418
+ callback?.();
15419
+ this.resolveFinished();
15420
+ });
15421
+ }
15422
+ newFinished() {
15423
+ this.isDone = false;
15424
+ this.finished = new Promise((resolve) => {
15425
+ this.resolveFinished = resolve;
15426
+ });
15427
+ }
15428
+ }
15429
+
14891
15430
  function isSequence(value) {
14892
15431
  return Array.isArray(value) && value.some(Array.isArray);
14893
15432
  }
@@ -14901,8 +15440,6 @@
14901
15440
  * Implementation
14902
15441
  */
14903
15442
  function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
14904
- let animations = [];
14905
- let animationOnComplete;
14906
15443
  const motionScope = activeScope.current;
14907
15444
  const scopeTransition = motionScope?.transition;
14908
15445
  const inherited = {};
@@ -14910,45 +15447,53 @@
14910
15447
  inherited.reduceMotion = reduceMotion;
14911
15448
  if (skipAnimations !== undefined)
14912
15449
  inherited.skipAnimations = skipAnimations;
15450
+ let timeline;
14913
15451
  if (isSequence(subjectOrSequence)) {
14914
- const { onComplete, ...sequenceOptions } = optionsOrKeyframes || {};
14915
- if (typeof onComplete === "function") {
14916
- animationOnComplete = onComplete;
14917
- }
15452
+ const sequenceOptions = {
15453
+ ...inherited,
15454
+ ...optionsOrKeyframes,
15455
+ scope,
15456
+ };
14918
15457
  if (scopeTransition) {
14919
15458
  sequenceOptions.defaultTransition = {
14920
15459
  ...scopeTransition,
14921
15460
  ...sequenceOptions.defaultTransition,
14922
15461
  };
14923
15462
  }
14924
- animations = animateSequence(subjectOrSequence, { ...inherited, ...sequenceOptions }, scope);
15463
+ timeline = new Timeline(subjectOrSequence, sequenceOptions);
14925
15464
  }
14926
15465
  else {
14927
15466
  // Extract top-level onComplete so it doesn't get applied per-value
14928
15467
  const { onComplete, ...rest } = options || {};
14929
- if (typeof onComplete === "function") {
14930
- animationOnComplete = onComplete;
14931
- }
14932
- animations = animateSubject(subjectOrSequence, optionsOrKeyframes, {
15468
+ const transition = {
14933
15469
  ...inherited,
14934
15470
  ...scopeTransition,
14935
15471
  ...rest,
14936
- }, scope);
14937
- }
14938
- const animation = new GroupAnimationWithThen(animations);
14939
- if (animationOnComplete) {
14940
- animation.finished.then(animationOnComplete);
15472
+ };
15473
+ /**
15474
+ * Play the animations directly, and only compile them as a
15475
+ * sequence if the timeline is changed. Values animated from a
15476
+ * number are animated via their onUpdate callback instead.
15477
+ */
15478
+ const { onUpdate } = rest;
15479
+ const segment = (typeof subjectOrSequence === "number" ||
15480
+ (typeof subjectOrSequence === "string" &&
15481
+ (typeof optionsOrKeyframes === "string" ||
15482
+ Array.isArray(optionsOrKeyframes)))
15483
+ ? [
15484
+ onUpdate || (() => { }),
15485
+ Array.isArray(optionsOrKeyframes)
15486
+ ? optionsOrKeyframes
15487
+ : [subjectOrSequence, optionsOrKeyframes],
15488
+ { ...transition, onUpdate: undefined },
15489
+ ]
15490
+ : [subjectOrSequence, optionsOrKeyframes, transition]);
15491
+ timeline = new Timeline([segment], { ...inherited, onComplete, scope }, animateSubject(subjectOrSequence, optionsOrKeyframes, transition, scope));
14941
15492
  }
14942
15493
  if (motionScope) {
14943
- animation.finished.then(motionScope.add(() => animation.stop()));
15494
+ timeline.finished.then(motionScope.add(() => timeline.stop()));
14944
15495
  }
14945
- if (scope) {
14946
- scope.animations.push(animation);
14947
- animation.finished.then(() => {
14948
- removeItem(scope.animations, animation);
14949
- });
14950
- }
14951
- return animation;
15496
+ return timeline;
14952
15497
  }
14953
15498
  return scopedAnimate;
14954
15499
  }
@@ -14971,7 +15516,9 @@
14971
15516
  }
14972
15517
  const elements = resolveElements(elementOrSelector, scope);
14973
15518
  const numElements = elements.length;
14974
- exports.invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
15519
+ typeof process !== "undefined" &&
15520
+ "development" !== "production" &&
15521
+ exports.invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
14975
15522
  /**
14976
15523
  * WAAPI doesn't support interrupting animations.
14977
15524
  *
@@ -15280,22 +15827,26 @@
15280
15827
  const p = (i + progress(at(i), at(i + 1), v)) / n;
15281
15828
  return reverse ? 1 - p : p;
15282
15829
  }
15283
- function resolveOffsets(info, options) {
15284
- const { offset: offsetDefinition = ScrollOffset.All, axis = "y" } = options;
15285
- const axisInfo = info[axis];
15286
- /**
15287
- * Reset the length of the resolved offset array rather than creating a new one.
15288
- */
15289
- axisInfo.offset.length = 0;
15290
- /**
15291
- * Populate the offset array by resolving the user's offset definition into
15292
- * a list of pixel scroll offsets.
15293
- */
15294
- const numOffsets = offsetDefinition.length;
15295
- for (let i = 0; i < numOffsets; i++) {
15296
- axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
15830
+ /**
15831
+ * Resolves offsets on both axes, as useScroll reports the target's progress
15832
+ * on each (#2546). `axis` only picks which progress scroll() reports.
15833
+ */
15834
+ function resolveOffsets(info, { offset: offsetDefinition = ScrollOffset.All }) {
15835
+ for (const axis in axisKeys) {
15836
+ const axisInfo = info[axis];
15837
+ /**
15838
+ * Reset the length of the resolved offset array rather than creating a new one.
15839
+ */
15840
+ axisInfo.offset.length = 0;
15841
+ /**
15842
+ * Populate the offset array by resolving the user's offset definition into
15843
+ * a list of pixel scroll offsets.
15844
+ */
15845
+ for (let i = 0; i < offsetDefinition.length; i++) {
15846
+ axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
15847
+ }
15848
+ axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
15297
15849
  }
15298
- axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
15299
15850
  }
15300
15851
 
15301
15852
  function calcInset(element, container) {
@@ -15801,6 +16352,7 @@
15801
16352
  exports.buildSVGAttrs = buildSVGAttrs;
15802
16353
  exports.buildSVGPath = buildSVGPath;
15803
16354
  exports.buildTransform = buildTransform$1;
16355
+ exports.buildTransformer = buildTransformer;
15804
16356
  exports.calcAxisDelta = calcAxisDelta;
15805
16357
  exports.calcBoxDelta = calcBoxDelta;
15806
16358
  exports.calcChildStagger = calcChildStagger;