motion 14.6.0 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -4417,9 +4404,8 @@
4417
4404
  * If we can't animate this value with the resolved keyframes
4418
4405
  * then we should complete it immediately.
4419
4406
  */
4420
- let canAnimateValue = true;
4421
- if (!canAnimate(keyframes, name, type, velocity)) {
4422
- canAnimateValue = false;
4407
+ const canAnimateValue = canAnimate(keyframes, name, type, velocity);
4408
+ if (!canAnimateValue) {
4423
4409
  if (MotionGlobalConfig.instantAnimations || !delay) {
4424
4410
  onUpdate?.(getFinalKeyframe(keyframes, options, finalKeyframe));
4425
4411
  }
@@ -4563,9 +4549,7 @@
4563
4549
  this.animation.complete();
4564
4550
  }
4565
4551
  cancel() {
4566
- if (this._animation) {
4567
- this.animation.cancel();
4568
- }
4552
+ this._animation?.cancel();
4569
4553
  this.keyframeResolver?.cancel();
4570
4554
  }
4571
4555
  }
@@ -4748,7 +4732,7 @@
4748
4732
  this.setCurrent(v);
4749
4733
  // Update update subscribers
4750
4734
  if (this.current !== this.prev) {
4751
- this.notifyChange();
4735
+ this.dirty();
4752
4736
  if (this.dependents) {
4753
4737
  for (const dependent of this.dependents) {
4754
4738
  dependent.dirty();
@@ -4886,12 +4870,9 @@
4886
4870
  * @public
4887
4871
  */
4888
4872
  set(v) {
4889
- if (!this.passiveEffect) {
4890
- this.updateAndNotify(v);
4891
- }
4892
- else {
4893
- this.passiveEffect(v, this.updateAndNotify);
4894
- }
4873
+ this.passiveEffect
4874
+ ? this.passiveEffect(v, this.updateAndNotify)
4875
+ : this.updateAndNotify(v);
4895
4876
  }
4896
4877
  setWithVelocity(prev, current, delta) {
4897
4878
  this.set(current);
@@ -4908,13 +4889,9 @@
4908
4889
  this.prev = v;
4909
4890
  this.prevUpdatedAt = this.prevFrameValue = undefined;
4910
4891
  endAnimation && this.stop();
4911
- if (this.stopPassiveEffect)
4912
- this.stopPassiveEffect();
4892
+ this.stopPassiveEffect?.();
4913
4893
  }
4914
4894
  dirty() {
4915
- this.notifyChange();
4916
- }
4917
- notifyChange() {
4918
4895
  const { current, changeSubscriber } = this;
4919
4896
  /**
4920
4897
  * One or the other exists. Subscribing during the direct
@@ -4929,15 +4906,10 @@
4929
4906
  }
4930
4907
  }
4931
4908
  addDependent(dependent) {
4932
- if (!this.dependents) {
4933
- this.dependents = new Set();
4934
- }
4935
- this.dependents.add(dependent);
4909
+ (this.dependents || (this.dependents = new Set())).add(dependent);
4936
4910
  }
4937
4911
  removeDependent(dependent) {
4938
- if (this.dependents) {
4939
- this.dependents.delete(dependent);
4940
- }
4912
+ this.dependents?.delete(dependent);
4941
4913
  }
4942
4914
  /**
4943
4915
  * Returns the latest state of `MotionValue`
@@ -4947,9 +4919,7 @@
4947
4919
  * @public
4948
4920
  */
4949
4921
  get() {
4950
- if (collectMotionValues.current) {
4951
- collectMotionValues.current.push(this);
4952
- }
4922
+ collectMotionValues.current?.push(this);
4953
4923
  /**
4954
4924
  * A value animating on the compositor isn't updated every frame,
4955
4925
  * so read it from its animation.
@@ -5015,7 +4985,7 @@
5015
4985
  isComplete = true;
5016
4986
  this.events.animationComplete?.notify();
5017
4987
  if (this.animation === animation)
5018
- this.clearAnimation();
4988
+ this.animation = undefined;
5019
4989
  resolve();
5020
4990
  });
5021
4991
  if (!isComplete)
@@ -5031,11 +5001,10 @@
5031
5001
  stop() {
5032
5002
  if (this.animation) {
5033
5003
  this.animation.stop();
5034
- if (this.events.animationCancel) {
5035
- this.events.animationCancel.notify();
5036
- }
5004
+ this.events.animationCancel?.notify();
5037
5005
  }
5038
- this.clearAnimation();
5006
+ // Assign rather than delete to keep the object's shape stable
5007
+ this.animation = undefined;
5039
5008
  }
5040
5009
  /**
5041
5010
  * Returns `true` if this value is currently animating.
@@ -5045,10 +5014,6 @@
5045
5014
  isAnimating() {
5046
5015
  return !!this.animation;
5047
5016
  }
5048
- clearAnimation() {
5049
- // Assign rather than delete to keep the object's shape stable
5050
- this.animation = undefined;
5051
- }
5052
5017
  /**
5053
5018
  * Destroy and clean up subscribers to this `MotionValue`.
5054
5019
  *
@@ -5063,9 +5028,7 @@
5063
5028
  this.events.destroy?.notify();
5064
5029
  this.clearListeners();
5065
5030
  this.stop();
5066
- if (this.stopPassiveEffect) {
5067
- this.stopPassiveEffect();
5068
- }
5031
+ this.stopPassiveEffect?.();
5069
5032
  }
5070
5033
  }
5071
5034
  function motionValue(init, options) {
@@ -5143,6 +5106,7 @@
5143
5106
  "repeatDelay",
5144
5107
  "from",
5145
5108
  "elapsed",
5109
+ "out",
5146
5110
  ]);
5147
5111
  /**
5148
5112
  * Decide whether a transition is defined on a given Transition.
@@ -5613,7 +5577,7 @@
5613
5577
  }
5614
5578
  const valueTransition = {
5615
5579
  delay,
5616
- ...getValueTransition$1(transition || {}, key),
5580
+ ...getValueTransition$1(animationTypeState?.out[key] || transition || {}, key),
5617
5581
  };
5618
5582
  if (skipAnimations)
5619
5583
  valueTransition.skipAnimations = true;
@@ -5783,8 +5747,9 @@
5783
5747
  }
5784
5748
 
5785
5749
  class DOMKeyframesResolver extends KeyframeResolver {
5786
- constructor(unresolvedKeyframes, onComplete, name, motionValue, element) {
5787
- super(unresolvedKeyframes, onComplete, name, motionValue, element, true);
5750
+ constructor() {
5751
+ super(...arguments);
5752
+ this.isAsync = true;
5788
5753
  }
5789
5754
  /**
5790
5755
  * Keyframes that don't need reading from the DOM, or measuring,
@@ -5812,7 +5777,7 @@
5812
5777
  }
5813
5778
  readKeyframes() {
5814
5779
  const { unresolvedKeyframes, element, name } = this;
5815
- if (!element || !element.current)
5780
+ if (!element?.current)
5816
5781
  return;
5817
5782
  super.readKeyframes();
5818
5783
  /**
@@ -5907,11 +5872,10 @@
5907
5872
  }
5908
5873
  measureInitialState() {
5909
5874
  const { element, unresolvedKeyframes, name } = this;
5910
- if (!element || !element.current)
5875
+ if (!element?.current)
5911
5876
  return;
5912
- if (name === "height") {
5877
+ if (name === "height")
5913
5878
  this.suspendedScrollY = window.pageYOffset;
5914
- }
5915
5879
  this.measuredOrigin = this.measure();
5916
5880
  unresolvedKeyframes[0] = this.measuredOrigin;
5917
5881
  // Set final key frame to measure after next render
@@ -5922,7 +5886,7 @@
5922
5886
  }
5923
5887
  measureEndState() {
5924
5888
  const { element, unresolvedKeyframes } = this;
5925
- if (!element || !element.current)
5889
+ if (!element?.current)
5926
5890
  return;
5927
5891
  this.motionValue?.jump(this.measuredOrigin, false);
5928
5892
  const finalKeyframeIndex = unresolvedKeyframes.length - 1;
@@ -5931,14 +5895,6 @@
5931
5895
  if (finalKeyframe !== null && this.finalKeyframe === undefined) {
5932
5896
  this.finalKeyframe = finalKeyframe;
5933
5897
  }
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
5898
  this.resolveNoneKeyframes();
5943
5899
  }
5944
5900
  }
@@ -6043,7 +5999,8 @@
6043
5999
  */
6044
6000
  const effects = [];
6045
6001
  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");
6002
+ exports.invariant(typeof effect.test === "function" &&
6003
+ typeof effect.read === "function", "Effects passed to animate.addEffect() need test() and read().", "effect-missing-test");
6047
6004
  removeEffect(effect);
6048
6005
  effects.unshift(effect);
6049
6006
  }
@@ -6370,11 +6327,6 @@
6370
6327
  });
6371
6328
  }
6372
6329
 
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
6330
  function buildTransform(state) {
6379
6331
  let transform = "";
6380
6332
  const { transformKeys: keys = [], transformValues: values = {} } = state;
@@ -6387,14 +6339,8 @@
6387
6339
  const value = values[key].getCurrent();
6388
6340
  if (value === undefined)
6389
6341
  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
- ")";
6342
+ if (!isDefaultTransform(key, value)) {
6343
+ transform += (transform && " ") + transformFunction(key, value);
6398
6344
  }
6399
6345
  }
6400
6346
  // See build-transform.ts: additive `rotate()` so user `rotate` isn't
@@ -6402,10 +6348,7 @@
6402
6348
  const pathRotation = state.get("pathRotation")?.get();
6403
6349
  if (pathRotation) {
6404
6350
  transform +=
6405
- (transform && " ") +
6406
- "rotate(" +
6407
- getValueAsType(pathRotation, transformValueTypes.pathRotation) +
6408
- ")";
6351
+ (transform && " ") + transformFunction("rotate", pathRotation);
6409
6352
  }
6410
6353
  return transform || "none";
6411
6354
  }
@@ -6808,10 +6751,24 @@
6808
6751
  !isIdentityScale(scaleX) ||
6809
6752
  !isIdentityScale(scaleY));
6810
6753
  }
6754
+ /**
6755
+ * Rotations and skews, which projection can't correct for.
6756
+ */
6757
+ const angleTransforms = [
6758
+ "rotate",
6759
+ "rotateX",
6760
+ "rotateY",
6761
+ "rotateZ",
6762
+ "skewX",
6763
+ "skewY",
6764
+ ];
6811
6765
  function hasTransform(values) {
6812
6766
  return (hasScale(values) ||
6813
6767
  has2DTranslate(values) ||
6814
- values.z ||
6768
+ hasDistortingTransform(values));
6769
+ }
6770
+ function hasDistortingTransform(values) {
6771
+ return (values.z ||
6815
6772
  values.rotate ||
6816
6773
  values.rotateX ||
6817
6774
  values.rotateY ||
@@ -6864,6 +6821,11 @@
6864
6821
  * in a tree upon our box before then calculating how to project it into our desired viewport-relative box
6865
6822
  *
6866
6823
  * This is the final nested loop within updateLayoutDelta for future refactoring
6824
+ *
6825
+ * isSharedTransition is true for a shared layout animation in page space, which
6826
+ * also applies ancestor scroll and transforms. Or it's the layoutRoot a shared
6827
+ * animation is relative to, so only transforms of ancestors not shared with it
6828
+ * (or above it) are applied.
6867
6829
  */
6868
6830
  function applyTreeDeltas(box, treeScale, treePath, isSharedTransition = false) {
6869
6831
  const treeLength = treePath.length;
@@ -6880,13 +6842,10 @@
6880
6842
  * TODO: Prefer to remove this, but currently we have motion components with
6881
6843
  * display: contents in Framer.
6882
6844
  */
6883
- const { visualElement } = node.options;
6884
- if (visualElement &&
6885
- visualElement.props.style &&
6886
- visualElement.props.style.display === "contents") {
6845
+ if (node.options.visualElement?.props.style?.display === "contents") {
6887
6846
  continue;
6888
6847
  }
6889
- if (isSharedTransition &&
6848
+ if (isSharedTransition === true &&
6890
6849
  node.options.layoutScroll &&
6891
6850
  node.scroll &&
6892
6851
  node !== node.root) {
@@ -6900,7 +6859,9 @@
6900
6859
  // Apply each ancestor's calculated delta into this component's recorded layout box
6901
6860
  applyBoxDelta(box, delta);
6902
6861
  }
6903
- if (isSharedTransition && hasTransform(node.latestValues)) {
6862
+ if (isSharedTransition &&
6863
+ (isSharedTransition.path?.[i] || isSharedTransition) !== node &&
6864
+ hasTransform(node.latestValues)) {
6904
6865
  transformBox(box, node.latestValues, node.layout?.layoutBox);
6905
6866
  }
6906
6867
  }
@@ -7167,15 +7128,6 @@
7167
7128
  const asNumber = (v) => typeof v === "number" ? v : parseFloat(v);
7168
7129
 
7169
7130
  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
7131
  const isPx = (value) => typeof value === "number" || px.test(value);
7180
7132
  function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
7181
7133
  if (shouldCrossfadeOpacity) {
@@ -7213,8 +7165,8 @@
7213
7165
  * Mix rotation and skew. These can't be projected, so mix them every
7214
7166
  * frame rather than leaving them frozen at the animation-start snapshot.
7215
7167
  */
7216
- for (let i = 0; i < numMixedTransforms; i++) {
7217
- const key = mixedTransforms[i];
7168
+ for (let i = 0; i < angleTransforms.length; i++) {
7169
+ const key = angleTransforms[i];
7218
7170
  if (follow[key] || lead[key]) {
7219
7171
  target[key] = mixNumber$1(follow[key] || 0, lead[key] || 0, progress);
7220
7172
  }
@@ -7297,8 +7249,8 @@
7297
7249
  }
7298
7250
  }
7299
7251
  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);
7252
+ calcAxisDelta(delta.x, source.x, target.x, origin?.originX);
7253
+ calcAxisDelta(delta.y, source.y, target.y, origin?.originY);
7302
7254
  }
7303
7255
  function calcRelativeAxis(target, relative, parent, anchor = 0) {
7304
7256
  const anchorPoint = anchor
@@ -7368,8 +7320,8 @@
7368
7320
  * and acts as a bridge between motion values and removeAxisDelta
7369
7321
  */
7370
7322
  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);
7323
+ removeAxisTransforms(box.x, transforms, xKeys, originBox?.x, sourceBox?.x);
7324
+ removeAxisTransforms(box.y, transforms, yKeys, originBox?.y, sourceBox?.y);
7373
7325
  }
7374
7326
 
7375
7327
  const createAxisDelta = () => ({
@@ -7599,24 +7551,18 @@
7599
7551
  transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
7600
7552
  }
7601
7553
  if (latestTransform) {
7602
- const { transformPerspective, rotate, pathRotation, rotateX, rotateY, rotateZ, skewX, skewY, } = latestTransform;
7554
+ const { transformPerspective } = latestTransform;
7603
7555
  if (transformPerspective)
7604
7556
  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) `;
7557
+ for (let i = 0; i < angleTransforms.length; i++) {
7558
+ const key = angleTransforms[i];
7559
+ const value = latestTransform[key];
7560
+ if (value)
7561
+ transform += `${key}(${value}deg) `;
7562
+ // Additive `rotate()` so user `rotate` isn't clobbered.
7563
+ if (!i && latestTransform.pathRotation)
7564
+ transform += `rotate(${latestTransform.pathRotation}deg) `;
7565
+ }
7620
7566
  }
7621
7567
  /**
7622
7568
  * Apply scale to match the size of the element to the size we want it.
@@ -7679,7 +7625,10 @@
7679
7625
  calculatedTargetDeltas: 0,
7680
7626
  calculatedProjections: 0,
7681
7627
  };
7682
- const transformAxes = ["", "X", "Y", "Z"];
7628
+ /**
7629
+ * Transforms that distort an element's layout, so must be reset before measuring.
7630
+ */
7631
+ const distortingTransforms = ["z", "skew", "skewZ"];
7683
7632
  /**
7684
7633
  * We use 1000 as the animation target as 0-1000 maps better to pixels than 0-1
7685
7634
  * which has a noticeable difference in spring animations
@@ -7887,8 +7836,7 @@
7887
7836
  return this.eventHandlers.get(name).add(handler);
7888
7837
  }
7889
7838
  notifyListeners(name, ...args) {
7890
- const subscriptionManager = this.eventHandlers.get(name);
7891
- subscriptionManager && subscriptionManager.notify(...args);
7839
+ this.eventHandlers.get(name)?.notify(...args);
7892
7840
  }
7893
7841
  hasListeners(name) {
7894
7842
  return this.eventHandlers.has(name);
@@ -7906,7 +7854,7 @@
7906
7854
  visualElement.mount(instance);
7907
7855
  }
7908
7856
  this.root.nodes.add(this);
7909
- this.parent && this.parent.children.add(this);
7857
+ this.parent?.children.add(this);
7910
7858
  if (this.root.hasTreeAnimated && (layout || layoutId)) {
7911
7859
  this.isLayoutDirty = true;
7912
7860
  }
@@ -7924,7 +7872,7 @@
7924
7872
  return;
7925
7873
  innerWidth = newInnerWidth;
7926
7874
  this.root.updateBlockedByResize = true;
7927
- cancelDelay && cancelDelay();
7875
+ cancelDelay?.();
7928
7876
  cancelDelay = delay(resizeUnblockUpdate, 250);
7929
7877
  if (globalProjectionState.hasAnimatedSinceResize) {
7930
7878
  globalProjectionState.hasAnimatedSinceResize = false;
@@ -8015,9 +7963,7 @@
8015
7963
  if (!hasLayoutChanged) {
8016
7964
  finishAnimation(this);
8017
7965
  }
8018
- if (this.isLead() && this.options.onExitComplete) {
8019
- this.options.onExitComplete();
8020
- }
7966
+ this.isLead() && this.options.onExitComplete?.();
8021
7967
  }
8022
7968
  this.targetLayout = newLayout;
8023
7969
  });
@@ -8025,9 +7971,8 @@
8025
7971
  unmount() {
8026
7972
  this.options.layoutId && this.willUpdate();
8027
7973
  this.root.nodes.remove(this);
8028
- const stack = this.getStack();
8029
- stack && stack.remove(this);
8030
- this.parent && this.parent.children.delete(this);
7974
+ this.getStack()?.remove(this);
7975
+ this.parent?.children.delete(this);
8031
7976
  this.instance = undefined;
8032
7977
  this.eventHandlers.clear();
8033
7978
  this.hasLayoutListener = false;
@@ -8048,7 +7993,7 @@
8048
7993
  if (this.isUpdateBlocked())
8049
7994
  return;
8050
7995
  this.isUpdating = true;
8051
- this.nodes && this.nodes.forEach(resetSkewAndRotation);
7996
+ this.nodes?.forEach(resetSkewAndRotation);
8052
7997
  this.animationId++;
8053
7998
  }
8054
7999
  getTransformTemplate() {
@@ -8058,7 +8003,7 @@
8058
8003
  willUpdate(shouldNotifyListeners = true) {
8059
8004
  this.root.hasTreeAnimated = true;
8060
8005
  if (this.root.isUpdateBlocked()) {
8061
- this.options.onExitComplete && this.options.onExitComplete();
8006
+ this.options.onExitComplete?.();
8062
8007
  return;
8063
8008
  }
8064
8009
  /**
@@ -8275,24 +8220,23 @@
8275
8220
  this.projectionDelta = undefined;
8276
8221
  this.notifyListeners("measure", this.layout.layoutBox);
8277
8222
  const { visualElement } = this.options;
8278
- visualElement &&
8279
- visualElement.notify("LayoutMeasure", this.layout.layoutBox, prevLayout ? prevLayout.layoutBox : undefined);
8223
+ visualElement?.notify("LayoutMeasure", this.layout.layoutBox, prevLayout?.layoutBox);
8280
8224
  }
8281
8225
  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);
8226
+ const { scroll, instance } = this;
8227
+ const { animationId } = this.root;
8228
+ if (this.options.layoutScroll &&
8229
+ instance &&
8230
+ !(scroll &&
8231
+ scroll.animationId === animationId &&
8232
+ scroll.phase === phase)) {
8233
+ const isRoot = checkIsScrollRoot(instance);
8290
8234
  this.scroll = {
8291
- animationId: this.root.animationId,
8235
+ animationId,
8292
8236
  phase,
8293
8237
  isRoot,
8294
- offset: measureScroll(this.instance),
8295
- wasRoot: this.scroll ? this.scroll.isRoot : isRoot,
8238
+ offset: measureScroll(instance),
8239
+ wasRoot: scroll ? scroll.isRoot : isRoot,
8296
8240
  };
8297
8241
  }
8298
8242
  }
@@ -8382,10 +8326,10 @@
8382
8326
  }
8383
8327
  return boxWithoutScroll;
8384
8328
  }
8385
- applyTransform(box, transformOnly = false, output, skipOwnTransform) {
8329
+ applyTransform(box, transformOnly = false, output, skipOwnTransform, start = 0) {
8386
8330
  const withTransforms = output || createBox();
8387
8331
  copyBoxInto(withTransforms, box);
8388
- for (let i = 0; i < this.path.length; i++) {
8332
+ for (let i = start; i < this.path.length; i++) {
8389
8333
  const node = this.path[i];
8390
8334
  if (!transformOnly &&
8391
8335
  node.options.layoutScroll &&
@@ -8557,8 +8501,13 @@
8557
8501
  this.attemptToResolveRelativeTarget = false;
8558
8502
  if (this.options.layoutAnchor !== false &&
8559
8503
  relativeParent &&
8560
- Boolean(relativeParent.resumingFrom) ===
8561
- Boolean(this.resumingFrom) &&
8504
+ (!relativeParent.resumingFrom === !this.resumingFrom ||
8505
+ /**
8506
+ * Shared layout animations within a layoutRoot
8507
+ * animate relative to it.
8508
+ */
8509
+ (relativeParent.options.layoutRoot &&
8510
+ !relativeParent.resumingFrom)) &&
8562
8511
  !relativeParent.options.layoutScroll &&
8563
8512
  relativeParent.target &&
8564
8513
  this.animationProgress !== 1) {
@@ -8638,7 +8587,7 @@
8638
8587
  * If this section of the tree isn't animating we can
8639
8588
  * delete our target sources for the following frame.
8640
8589
  */
8641
- this.isTreeAnimating = Boolean((this.parent && this.parent.isTreeAnimating) ||
8590
+ this.isTreeAnimating = Boolean(this.parent?.isTreeAnimating ||
8642
8591
  this.currentAnimation ||
8643
8592
  this.pendingAnimation);
8644
8593
  if (!this.isTreeAnimating) {
@@ -8659,8 +8608,17 @@
8659
8608
  /**
8660
8609
  * Apply all the parent deltas to this box to produce the corrected box. This
8661
8610
  * is the layout box, as it will appear on screen as a result of the transforms of its parents.
8611
+ *
8612
+ * A shared target is usually in page space, so ancestor transforms are applied
8613
+ * too. But a target relative to a parent that isn't itself shared (like a
8614
+ * layoutRoot) is in that parent's layout space. So the transforms of the
8615
+ * parent and its ancestors, which move both elements alike, are skipped,
8616
+ * but those of other ancestors (between it and either element) apply.
8662
8617
  */
8663
- applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared);
8618
+ applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared &&
8619
+ (lead.relativeTarget && !lead.relativeParent.resumingFrom
8620
+ ? lead.relativeParent
8621
+ : true));
8664
8622
  /**
8665
8623
  * If this layer needs to perform scale correction but doesn't have a target,
8666
8624
  * use the layout as the target.
@@ -8727,8 +8685,7 @@
8727
8685
  scheduleRender(notifyAll = true) {
8728
8686
  this.options.visualElement?.scheduleRender();
8729
8687
  if (notifyAll) {
8730
- const stack = this.getStack();
8731
- stack && stack.scheduleRender();
8688
+ this.getStack()?.scheduleRender();
8732
8689
  }
8733
8690
  if (this.resumingFrom && !this.resumingFrom.instance) {
8734
8691
  this.resumingFrom = undefined;
@@ -8750,9 +8707,7 @@
8750
8707
  }
8751
8708
  this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
8752
8709
  const relativeLayout = createBox();
8753
- const snapshotSource = snapshot ? snapshot.source : undefined;
8754
- const layoutSource = this.layout ? this.layout.source : undefined;
8755
- const isSharedLayoutAnimation = snapshotSource !== layoutSource;
8710
+ const isSharedLayoutAnimation = snapshot?.source !== this.layout?.source;
8756
8711
  const stack = this.getStack();
8757
8712
  const isOnlyMember = !stack || stack.members.length <= 1;
8758
8713
  const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
@@ -8767,19 +8722,11 @@
8767
8722
  this.mixTargetDelta = (latest) => {
8768
8723
  const progress = latest / 1000;
8769
8724
  const point = interpolate?.(progress);
8725
+ mixAxisDeltaLinear(targetDelta.x, delta.x, progress);
8726
+ mixAxisDeltaLinear(targetDelta.y, delta.y, progress);
8770
8727
  if (point) {
8771
8728
  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
8729
  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
8730
  }
8784
8731
  this.setTargetDelta(targetDelta);
8785
8732
  if (this.relativeTarget &&
@@ -8805,7 +8752,7 @@
8805
8752
  this.animationValues = mixedValues;
8806
8753
  mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
8807
8754
  }
8808
- if (point && point.rotate !== undefined) {
8755
+ if (point?.rotate !== undefined) {
8809
8756
  // Dedicated `pathRotation` channel, not `rotate`, so an
8810
8757
  // animating `rotate` is composed with, never clobbered.
8811
8758
  if (!this.animationValues)
@@ -8870,7 +8817,7 @@
8870
8817
  }
8871
8818
  finishAnimation() {
8872
8819
  if (this.currentAnimation) {
8873
- this.mixTargetDelta && this.mixTargetDelta(animationTarget);
8820
+ this.mixTargetDelta?.(animationTarget);
8874
8821
  this.currentAnimation.stop();
8875
8822
  }
8876
8823
  this.completeAnimation();
@@ -8890,13 +8837,11 @@
8890
8837
  this.layout &&
8891
8838
  layout &&
8892
8839
  shouldAnimatePositionOnly(this.options.animationType, this.layout.layoutBox, layout.layoutBox)) {
8840
+ const { layoutBox } = this.layout;
8841
+ const leadTarget = target;
8893
8842
  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;
8843
+ alignAxis(target.x, leadTarget.x, layoutBox.x);
8844
+ alignAxis(target.y, leadTarget.y, layoutBox.y);
8900
8845
  }
8901
8846
  copyBoxInto(targetWithTransforms, target);
8902
8847
  /**
@@ -8921,8 +8866,8 @@
8921
8866
  stack.add(node);
8922
8867
  const config = node.options.initialPromotionConfig;
8923
8868
  node.promote({
8924
- transition: config ? config.transition : undefined,
8925
- preserveFollowOpacity: config && config.shouldPreserveFollowOpacity
8869
+ transition: config?.transition,
8870
+ preserveFollowOpacity: config?.shouldPreserveFollowOpacity
8926
8871
  ? config.shouldPreserveFollowOpacity(node)
8927
8872
  : undefined,
8928
8873
  });
@@ -8945,9 +8890,7 @@
8945
8890
  return this.root.sharedNodes.get(layoutId);
8946
8891
  }
8947
8892
  promote({ needsReset, transition, preserveFollowOpacity, } = {}) {
8948
- const stack = this.getStack();
8949
- if (stack)
8950
- stack.promote(this, preserveFollowOpacity);
8893
+ this.getStack()?.promote(this, preserveFollowOpacity);
8951
8894
  if (needsReset) {
8952
8895
  this.projectionDelta = undefined;
8953
8896
  this.needsReset = true;
@@ -8956,45 +8899,27 @@
8956
8899
  this.setOptions({ transition });
8957
8900
  }
8958
8901
  relegate() {
8959
- const stack = this.getStack();
8960
- if (stack) {
8961
- return stack.relegate(this);
8962
- }
8963
- else {
8964
- return false;
8965
- }
8902
+ return this.getStack()?.relegate(this) || false;
8966
8903
  }
8967
8904
  resetSkewAndRotation() {
8968
8905
  const { visualElement } = this.options;
8969
8906
  if (!visualElement)
8970
8907
  return;
8971
- // If there's no detected skew or rotation values, we can early return without a forced render.
8972
- let hasDistortingTransform = false;
8973
8908
  /**
8974
8909
  * 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.
8910
+ * skipping the loop and new object creation is 50% faster.
8911
+ * If there's no distorting values, we don't need to do any more.
8976
8912
  */
8977
8913
  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)
8914
+ if (!hasDistortingTransform(latestValues))
8989
8915
  return;
8990
8916
  const resetValues = {};
8991
- if (latestValues.z) {
8992
- resetDistortingTransform("z", visualElement, resetValues, this.animationValues);
8993
- }
8994
8917
  // 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);
8918
+ for (let i = 0; i < distortingTransforms.length; i++) {
8919
+ resetDistortingTransform(distortingTransforms[i], visualElement, resetValues, this.animationValues);
8920
+ }
8921
+ for (let i = 0; i < angleTransforms.length; i++) {
8922
+ resetDistortingTransform(angleTransforms[i], visualElement, resetValues, this.animationValues);
8998
8923
  }
8999
8924
  // Force a render of this element to apply the transform with all skews and rotations
9000
8925
  // set to 0.
@@ -9014,7 +8939,13 @@
9014
8939
  styleProp) {
9015
8940
  if (!this.instance || this.isSVG)
9016
8941
  return;
9017
- if (!this.isVisible) {
8942
+ const lead = this.getLead();
8943
+ /**
8944
+ * Without crossfade, a new lead hides the previous one, as it's
8945
+ * usually unmounting. One that gave its layoutId away keeps
8946
+ * rendering, so only hide nodes that follow another.
8947
+ */
8948
+ if (!this.isVisible && lead !== this) {
9018
8949
  targetStyle.visibility = "hidden";
9019
8950
  return;
9020
8951
  }
@@ -9030,9 +8961,10 @@
9030
8961
  : "none";
9031
8962
  return;
9032
8963
  }
9033
- const lead = this.getLead();
9034
8964
  if (!this.projectionDelta || !this.layout || !lead.target) {
9035
- if (this.options.layoutId) {
8965
+ if (this.options.layoutId || this.needsShow) {
8966
+ this.needsShow = false;
8967
+ targetStyle.visibility = "";
9036
8968
  targetStyle.opacity =
9037
8969
  this.latestValues.opacity !== undefined
9038
8970
  ? this.latestValues.opacity
@@ -9166,52 +9098,49 @@
9166
9098
  const isShared = snapshot.source !== node.layout.source;
9167
9099
  // TODO Maybe we want to also resize the layout snapshot so we don't trigger
9168
9100
  // 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
- }
9101
+ const resizeSnapshot = (box, to) => {
9102
+ if (animationType === "size") {
9103
+ eachAxis((axis) => {
9104
+ const length = calcLength(box[axis]);
9105
+ box[axis].min = to[axis].min;
9106
+ box[axis].max = box[axis].min + length;
9107
+ });
9108
+ }
9109
+ else if (animationType === "x" || animationType === "y") {
9110
+ const snapAxis = animationType === "x" ? "y" : "x";
9111
+ copyAxisInto(box[snapAxis], to[snapAxis]);
9112
+ }
9113
+ else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
9114
+ eachAxis((axis) => {
9115
+ const length = calcLength(to[axis]);
9116
+ box[axis].max = box[axis].min + length;
9117
+ /**
9118
+ * Ensure relative target gets resized and rerendererd
9119
+ */
9120
+ if (node.relativeTarget && !node.currentAnimation) {
9121
+ node.isProjectionDirty = true;
9122
+ node.relativeTarget[axis].max =
9123
+ node.relativeTarget[axis].min + length;
9124
+ }
9125
+ });
9126
+ }
9127
+ };
9128
+ const snapshotBox = isShared ? snapshot.measuredBox : snapshot.layoutBox;
9129
+ resizeSnapshot(snapshotBox, layout);
9202
9130
  const layoutDelta = createDelta();
9203
9131
  calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
9204
9132
  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
- }
9133
+ calcBoxDelta(visualDelta, isShared ? node.applyTransform(measuredLayout, true) : layout, snapshotBox);
9211
9134
  const hasLayoutChanged = !isDeltaZero(layoutDelta);
9212
9135
  let hasRelativeLayoutChanged = false;
9213
- if (!node.resumeFrom) {
9214
- const relativeParent = node.getClosestProjectingParent();
9136
+ const relativeParent = node.getClosestProjectingParent();
9137
+ /**
9138
+ * A snapshot resumed from another element is only relative to this
9139
+ * node's relativeParent if it's a layoutRoot that contains both.
9140
+ */
9141
+ if (!node.resumeFrom ||
9142
+ (relativeParent?.options.layoutRoot &&
9143
+ node.resumeFrom.path.includes(relativeParent))) {
9215
9144
  /**
9216
9145
  * If the relativeParent is itself resuming from a different element then
9217
9146
  * the relative snapshot is not relavent
@@ -9220,10 +9149,22 @@
9220
9149
  const { snapshot: parentSnapshot, layout: parentLayout } = relativeParent;
9221
9150
  if (parentSnapshot && parentLayout) {
9222
9151
  const anchor = node.options.layoutAnchor || undefined;
9152
+ /**
9153
+ * A shared snapshot includes the transforms of the old
9154
+ * element's ancestors below the layoutRoot, as they
9155
+ * aren't shared with the new element.
9156
+ */
9223
9157
  const relativeSnapshot = createBox();
9224
- calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
9158
+ calcRelativePosition(relativeSnapshot, isShared
9159
+ ? node.resumeFrom.applyTransform(snapshot.layoutBox, true, undefined, true, relativeParent.depth + 1)
9160
+ : snapshot.layoutBox, parentSnapshot.layoutBox, anchor);
9225
9161
  const relativeLayout = createBox();
9226
9162
  calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox, anchor);
9163
+ /**
9164
+ * A shared snapshot is resized in page space, above, so
9165
+ * resize it relative to the layoutRoot too.
9166
+ */
9167
+ isShared && resizeSnapshot(relativeSnapshot, relativeLayout);
9227
9168
  if (!boxEqualsRounded(relativeSnapshot, relativeLayout)) {
9228
9169
  hasRelativeLayoutChanged = true;
9229
9170
  }
@@ -9245,8 +9186,7 @@
9245
9186
  });
9246
9187
  }
9247
9188
  else if (node.isLead()) {
9248
- const { onExitComplete } = node.options;
9249
- onExitComplete && onExitComplete();
9189
+ node.options.onExitComplete?.();
9250
9190
  }
9251
9191
  /**
9252
9192
  * Clearing transition
@@ -9352,7 +9292,7 @@
9352
9292
  }
9353
9293
  function resetTransformStyle(node) {
9354
9294
  const { visualElement } = node.options;
9355
- if (visualElement && visualElement.getProps().onBeforeLayoutMeasure) {
9295
+ if (visualElement?.getProps().onBeforeLayoutMeasure) {
9356
9296
  visualElement.notify("BeforeLayoutMeasure");
9357
9297
  }
9358
9298
  node.resetTransform();
@@ -9386,7 +9326,7 @@
9386
9326
  mixAxis(output.y, from.y, to.y, p);
9387
9327
  }
9388
9328
  function hasOpacityCrossfade(node) {
9389
- return (node.animationValues && node.animationValues.opacityExit !== undefined);
9329
+ return node.animationValues?.opacityExit !== undefined;
9390
9330
  }
9391
9331
  const defaultLayoutTransition = {
9392
9332
  duration: 0.45,
@@ -9412,6 +9352,13 @@
9412
9352
  roundAxis(box.x);
9413
9353
  roundAxis(box.y);
9414
9354
  }
9355
+ /**
9356
+ * Align an axis to the start of another, keeping the length of a layout axis.
9357
+ * The length is read first, as output can be the layout axis itself.
9358
+ */
9359
+ function alignAxis(output, from, layout) {
9360
+ output.max = calcLength(layout) + (output.min = from.min);
9361
+ }
9415
9362
  function shouldAnimatePositionOnly(animationType, snapshot, layout) {
9416
9363
  return (animationType === "position" ||
9417
9364
  (animationType === "preserve-aspect" &&
@@ -9462,7 +9409,7 @@
9462
9409
  resetTransform: (instance, value) => {
9463
9410
  instance.style.transform = value !== undefined ? value : "none";
9464
9411
  },
9465
- checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9412
+ checkIsScrollRoot: (instance) => window.getComputedStyle(instance).position === "fixed",
9466
9413
  });
9467
9414
 
9468
9415
  const visualElementStore = new WeakMap();
@@ -9748,9 +9695,7 @@
9748
9695
  }
9749
9696
  this.current = instance;
9750
9697
  visualElementStore.set(instance, this);
9751
- if (this.projection && !this.projection.instance) {
9752
- this.projection.mount(instance);
9753
- }
9698
+ this.projection?.instance || this.projection?.mount(instance);
9754
9699
  /**
9755
9700
  * Join the closest variant node above, so its variant changes
9756
9701
  * propagate here, unless inherit={false} cuts the chain.
@@ -9796,12 +9741,12 @@
9796
9741
  this.hasBeenMounted = true;
9797
9742
  }
9798
9743
  unmount() {
9799
- this.projection && this.projection.unmount();
9744
+ this.projection?.unmount();
9800
9745
  cancelFrame(this.notifyUpdate);
9801
9746
  cancelFrame(this.render);
9802
9747
  this.valueSubscriptions.forEach((remove) => remove());
9803
9748
  this.valueSubscriptions.clear();
9804
- this.removeFromVariantTree && this.removeFromVariantTree();
9749
+ this.removeFromVariantTree?.();
9805
9750
  this.parent?.removeChild(this);
9806
9751
  for (const key in this.events) {
9807
9752
  this.events[key].clear();
@@ -9817,17 +9762,14 @@
9817
9762
  }
9818
9763
  addChild(child) {
9819
9764
  this.children.add(child);
9820
- this.enteringChildren ?? (this.enteringChildren = new Set());
9821
- this.enteringChildren.add(child);
9765
+ (this.enteringChildren || (this.enteringChildren = new Set())).add(child);
9822
9766
  }
9823
9767
  removeChild(child) {
9824
9768
  this.children.delete(child);
9825
- this.enteringChildren && this.enteringChildren.delete(child);
9769
+ this.enteringChildren?.delete(child);
9826
9770
  }
9827
9771
  bindToMotionValue(key, value) {
9828
- if (this.valueSubscriptions.has(key)) {
9829
- this.valueSubscriptions.get(key)();
9830
- }
9772
+ this.valueSubscriptions.get(key)?.();
9831
9773
  if (value.accelerate &&
9832
9774
  acceleratedValues.has(key) &&
9833
9775
  this.current instanceof HTMLElement) {
@@ -9869,8 +9811,7 @@
9869
9811
  }
9870
9812
  this.valueSubscriptions.set(key, () => {
9871
9813
  removeOnChange();
9872
- if (removeSyncCheck)
9873
- removeSyncCheck();
9814
+ removeSyncCheck?.();
9874
9815
  // Defer to MotionValue.on("change") auto-stop so React 19 remounts
9875
9816
  // can resubscribe before the animation is cancelled (#3315).
9876
9817
  });
@@ -9963,10 +9904,8 @@
9963
9904
  */
9964
9905
  for (let i = 0; i < propEventHandlers.length; i++) {
9965
9906
  const key = propEventHandlers[i];
9966
- if (this.propEventSubscriptions[key]) {
9967
- this.propEventSubscriptions[key]();
9968
- delete this.propEventSubscriptions[key];
9969
- }
9907
+ this.propEventSubscriptions[key]?.();
9908
+ delete this.propEventSubscriptions[key];
9970
9909
  const listenerName = ("on" + key);
9971
9910
  const listener = props[listenerName];
9972
9911
  if (listener) {
@@ -9974,9 +9913,7 @@
9974
9913
  }
9975
9914
  }
9976
9915
  this.prevMotionValues = updateMotionValuesFromProps(this, this.scrapeMotionValuesFromProps(props, this.prevProps || {}, this), this.prevMotionValues);
9977
- if (this.handleChildMotionValue) {
9978
- this.handleChildMotionValue();
9979
- }
9916
+ this.handleChildMotionValue?.();
9980
9917
  }
9981
9918
  getProps() {
9982
9919
  return this.props;
@@ -9985,7 +9922,7 @@
9985
9922
  * Returns the variant definition with a given name.
9986
9923
  */
9987
9924
  getVariant(name) {
9988
- return this.props.variants ? this.props.variants[name] : undefined;
9925
+ return this.props.variants?.[name];
9989
9926
  }
9990
9927
  /**
9991
9928
  * Returns the defined default transition on this component.
@@ -10023,11 +9960,8 @@
10023
9960
  */
10024
9961
  removeValue(key) {
10025
9962
  this.values.delete(key);
10026
- const unsubscribe = this.valueSubscriptions.get(key);
10027
- if (unsubscribe) {
10028
- unsubscribe();
10029
- this.valueSubscriptions.delete(key);
10030
- }
9963
+ this.valueSubscriptions.get(key)?.();
9964
+ this.valueSubscriptions.delete(key);
10031
9965
  delete this.latestValues[key];
10032
9966
  this.removeValueFromRenderState(key, this.renderState);
10033
9967
  }
@@ -10038,10 +9972,10 @@
10038
9972
  return this.values.has(key);
10039
9973
  }
10040
9974
  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);
9975
+ let value = this.props.values?.[key];
9976
+ if (value)
9977
+ return value;
9978
+ value = this.values.get(key);
10045
9979
  if (value === undefined && defaultValue !== undefined) {
10046
9980
  value = motionValue(defaultValue ?? this.getDefaultValue?.(key), {
10047
9981
  owner: this,
@@ -10091,15 +10025,11 @@
10091
10025
  this.baseTarget[key] = value;
10092
10026
  }
10093
10027
  on(eventName, callback) {
10094
- if (!this.events[eventName]) {
10095
- this.events[eventName] = new SubscriptionManager();
10096
- }
10097
- return this.events[eventName].add(callback);
10028
+ return (this.events[eventName] ||
10029
+ (this.events[eventName] = new SubscriptionManager())).add(callback);
10098
10030
  }
10099
10031
  notify(eventName, ...args) {
10100
- if (this.events[eventName]) {
10101
- this.events[eventName].notify(...args);
10102
- }
10032
+ this.events[eventName]?.notify(...args);
10103
10033
  }
10104
10034
  scheduleRenderMicrotask() {
10105
10035
  microtask.render(this.render);
@@ -10272,7 +10202,7 @@
10272
10202
  let ancestor = element.parentElement;
10273
10203
  while (ancestor) {
10274
10204
  const node = layoutNodes.get(ancestor);
10275
- if (node && node.instance)
10205
+ if (node?.instance)
10276
10206
  return node;
10277
10207
  ancestor = ancestor.parentElement;
10278
10208
  }
@@ -10538,7 +10468,7 @@
10538
10468
  hasSurvivor &&
10539
10469
  !newMemberIds.has(layoutId)) {
10540
10470
  if (restore && restore.parent.isConnected) {
10541
- restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
10471
+ restore.parent.insertBefore(element, restore.next?.parentNode === restore.parent
10542
10472
  ? restore.next
10543
10473
  : null);
10544
10474
  node.isPresent = false;
@@ -10743,10 +10673,6 @@
10743
10673
  * Scroll positions of scrollable ancestors and window.
10744
10674
  */
10745
10675
  this.scrollPositions = new Map();
10746
- /**
10747
- * Cleanup function for scroll listeners.
10748
- */
10749
- this.removeScrollListeners = null;
10750
10676
  this.onElementScroll = (event) => {
10751
10677
  this.handleScroll(event.target);
10752
10678
  };
@@ -10774,10 +10700,10 @@
10774
10700
  this.history.push({ ...point, timestamp: time.now() });
10775
10701
  const { onStart, onMove } = this.handlers;
10776
10702
  if (!isPanStarted) {
10777
- onStart && onStart(this.lastMoveEvent, info);
10703
+ onStart?.(this.lastMoveEvent, info);
10778
10704
  this.startEvent = this.lastMoveEvent;
10779
10705
  }
10780
- onMove && onMove(this.lastMoveEvent, info);
10706
+ onMove?.(this.lastMoveEvent, info);
10781
10707
  };
10782
10708
  this.handlePointerMove = (event, info) => {
10783
10709
  /**
@@ -10802,17 +10728,15 @@
10802
10728
  // Resume animation if dragSnapToOrigin is set OR if no drag started (user just clicked)
10803
10729
  // This ensures constraint animations continue when interrupted by a click
10804
10730
  if (this.dragSnapToOrigin || !this.startEvent) {
10805
- resumeAnimation && resumeAnimation();
10731
+ resumeAnimation?.();
10806
10732
  }
10807
10733
  if (!(this.lastMoveEvent && this.lastMoveEventInfo))
10808
10734
  return;
10809
10735
  const panInfo = getPanInfo(event.type === "pointercancel"
10810
10736
  ? this.lastMoveEventInfo
10811
10737
  : transformPoint(info, this.transformPagePoint), this.history);
10812
- if (this.startEvent && onEnd) {
10813
- onEnd(event, panInfo);
10814
- }
10815
- onSessionEnd && onSessionEnd(event, panInfo);
10738
+ this.startEvent && onEnd?.(event, panInfo);
10739
+ onSessionEnd?.(event, panInfo);
10816
10740
  };
10817
10741
  // If we have more than one touch, don't start detecting this gesture
10818
10742
  if (!isPrimaryPointer(event))
@@ -10825,8 +10749,13 @@
10825
10749
  const info = extractEventInfo(event);
10826
10750
  const initialInfo = transformPoint(info, this.transformPagePoint);
10827
10751
  const { point } = initialInfo;
10828
- const { timestamp } = frameData;
10829
- this.history = [{ ...point, timestamp }];
10752
+ /**
10753
+ * Stamp the origin with time.now(), not frameData.timestamp: when
10754
+ * the frameloop is idle, frameData.timestamp is from the last frame
10755
+ * that ran, which can be seconds old, and a fast flick from a
10756
+ * standstill would then report near-zero velocity. See #1747.
10757
+ */
10758
+ this.history = [{ ...point, timestamp: time.now() }];
10830
10759
  const { onSessionStart } = handlers;
10831
10760
  onSessionStart &&
10832
10761
  onSessionStart(event, getPanInfo(initialInfo, this.history));
@@ -10850,30 +10779,19 @@
10850
10779
  const style = getComputedStyle(current);
10851
10780
  if (overflowStyles.has(style.overflowX) ||
10852
10781
  overflowStyles.has(style.overflowY)) {
10853
- this.scrollPositions.set(current, {
10854
- x: current.scrollLeft,
10855
- y: current.scrollTop,
10856
- });
10782
+ this.scrollPositions.set(current, getScroll(current));
10857
10783
  }
10858
10784
  current = current.parentElement;
10859
10785
  }
10860
10786
  // Track window scroll
10861
- this.scrollPositions.set(window, {
10862
- x: window.scrollX,
10863
- y: window.scrollY,
10864
- });
10787
+ this.scrollPositions.set(window, getScroll(window));
10788
+ this.removeScrollListeners = pipe(
10865
10789
  // Capture listener catches element scroll events as they bubble
10866
- window.addEventListener("scroll", this.onElementScroll, {
10790
+ addDomEvent(window, "scroll", this.onElementScroll, {
10867
10791
  capture: true,
10868
- });
10792
+ }),
10869
10793
  // 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
- };
10794
+ addDomEvent(window, "scroll", this.onWindowScroll, {}));
10877
10795
  }
10878
10796
  /**
10879
10797
  * Handle scroll compensation during drag.
@@ -10886,12 +10804,7 @@
10886
10804
  if (!initial)
10887
10805
  return;
10888
10806
  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
- };
10807
+ const current = getScroll(target);
10895
10808
  const delta = { x: current.x - initial.x, y: current.y - initial.y };
10896
10809
  if (delta.x === 0 && delta.y === 0)
10897
10810
  return;
@@ -10916,12 +10829,18 @@
10916
10829
  this.handlers = handlers;
10917
10830
  }
10918
10831
  end() {
10919
- this.removeListeners && this.removeListeners();
10920
- this.removeScrollListeners && this.removeScrollListeners();
10832
+ this.removeListeners?.();
10833
+ this.removeScrollListeners?.();
10921
10834
  this.scrollPositions.clear();
10922
10835
  cancelFrame(this.updatePoint);
10923
10836
  }
10924
10837
  }
10838
+ const getScroll = (target) => target === window
10839
+ ? { x: window.scrollX, y: window.scrollY }
10840
+ : {
10841
+ x: target.scrollLeft,
10842
+ y: target.scrollTop,
10843
+ };
10925
10844
  function transformPoint(info, transformPagePoint) {
10926
10845
  return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
10927
10846
  }
@@ -11178,6 +11097,25 @@
11178
11097
  : dragElastic[label] || 0;
11179
11098
  }
11180
11099
 
11100
+ /**
11101
+ * Re-read the scroll of the projection's ancestors, and return that of the
11102
+ * `layoutScroll` ones as a key that changes when any of them scroll.
11103
+ *
11104
+ * Clear the cached scroll first so `updateScroll` bypasses its
11105
+ * per-animationId cache and re-reads the live value.
11106
+ */
11107
+ function measureScroll(projection) {
11108
+ let key = "";
11109
+ for (const node of projection.path) {
11110
+ node.scroll = undefined;
11111
+ node.updateScroll();
11112
+ const { scroll } = node;
11113
+ if (node !== projection.root && scroll) {
11114
+ key += scroll.offset.x + "," + scroll.offset.y + ",";
11115
+ }
11116
+ }
11117
+ return key;
11118
+ }
11181
11119
  const elementDragControls = new WeakMap();
11182
11120
  class VisualElementDragControls {
11183
11121
  constructor(visualElement, readProps = () => visualElement.getProps()) {
@@ -11209,8 +11147,7 @@
11209
11147
  /**
11210
11148
  * Don't start dragging if this component is exiting
11211
11149
  */
11212
- const { presenceContext } = this.visualElement;
11213
- if (presenceContext && presenceContext.isPresent === false)
11150
+ if (this.visualElement.presenceContext?.isPresent === false)
11214
11151
  return;
11215
11152
  const onSessionStart = (event) => {
11216
11153
  if (snapToCursor)
@@ -11221,8 +11158,7 @@
11221
11158
  // Attempt to grab the global drag gesture lock - maybe make this part of PanSession
11222
11159
  const { drag, dragPropagation, onDragStart } = this.getProps();
11223
11160
  if (drag && !dragPropagation) {
11224
- if (this.openDragLock)
11225
- this.openDragLock();
11161
+ this.openDragLock?.();
11226
11162
  this.openDragLock = setDragLock(drag);
11227
11163
  // If we don 't have the lock, don't start dragging
11228
11164
  if (!this.openDragLock)
@@ -11233,9 +11169,10 @@
11233
11169
  this.isDragging = true;
11234
11170
  this.currentDirection = null;
11235
11171
  this.resolveConstraints();
11236
- if (this.visualElement.projection) {
11237
- this.visualElement.projection.isAnimationBlocked = true;
11238
- this.visualElement.projection.target = undefined;
11172
+ const { projection } = this.visualElement;
11173
+ if (projection) {
11174
+ projection.isAnimationBlocked = true;
11175
+ projection.target = undefined;
11239
11176
  }
11240
11177
  /**
11241
11178
  * Record gesture origin and pointer offset
@@ -11257,13 +11194,11 @@
11257
11194
  current = parseFloat(current);
11258
11195
  }
11259
11196
  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
- }
11197
+ const measuredAxis = projection?.layout?.layoutBox[axis];
11198
+ if (measuredAxis) {
11199
+ current =
11200
+ calcLength(measuredAxis) *
11201
+ (parseFloat(current) / 100);
11267
11202
  }
11268
11203
  }
11269
11204
  this.originPoint[axis] = current;
@@ -11273,8 +11208,7 @@
11273
11208
  frame.update(() => onDragStart(event, info), false, true);
11274
11209
  }
11275
11210
  addValueToWillChange(this.visualElement, "transform");
11276
- const { animationState } = this.visualElement;
11277
- animationState && animationState.setActive("whileDrag", true);
11211
+ this.visualElement.animationState?.setActive("whileDrag", true);
11278
11212
  };
11279
11213
  const onMove = (event, info) => {
11280
11214
  this.latestPointerEvent = event;
@@ -11297,8 +11231,8 @@
11297
11231
  onDirectionLock && onDirectionLock(this.currentDirection);
11298
11232
  }
11299
11233
  // Update each point with the latest position
11300
- this.updateAxis("x", info.point, offset);
11301
- this.updateAxis("y", info.point, offset);
11234
+ this.updateAxis("x", offset);
11235
+ this.updateAxis("y", offset);
11302
11236
  /**
11303
11237
  * Ideally we would leave the renderer to fire naturally at the end of
11304
11238
  * this frame but if the element is about to change layout as the result
@@ -11367,12 +11301,11 @@
11367
11301
  projection.isAnimationBlocked = false;
11368
11302
  }
11369
11303
  this.endPanSession();
11370
- const { dragPropagation } = this.getProps();
11371
- if (!dragPropagation && this.openDragLock) {
11304
+ if (!this.getProps().dragPropagation && this.openDragLock) {
11372
11305
  this.openDragLock();
11373
11306
  this.openDragLock = null;
11374
11307
  }
11375
- animationState && animationState.setActive("whileDrag", false);
11308
+ animationState?.setActive("whileDrag", false);
11376
11309
  }
11377
11310
  /**
11378
11311
  * Clean up the pan session without modifying other drag state.
@@ -11380,31 +11313,42 @@
11380
11313
  * without affecting projection animations or drag locks.
11381
11314
  */
11382
11315
  endPanSession() {
11383
- this.panSession && this.panSession.end();
11316
+ this.panSession?.end();
11384
11317
  this.panSession = undefined;
11385
11318
  }
11386
- updateAxis(axis, _point, offset) {
11387
- const { drag } = this.getProps();
11319
+ updateAxis(axis, offset) {
11320
+ /**
11321
+ * Read drag per axis: a callback fired while updating the x axis
11322
+ * can synchronously re-render with new props.
11323
+ */
11324
+ const { drag } = this.readProps();
11388
11325
  // If we're not dragging this axis, do an early return.
11389
11326
  if (!offset || !shouldDrag(axis, drag, this.currentDirection))
11390
11327
  return;
11391
11328
  const axisValue = this.getAxisMotionValue(axis);
11392
11329
  let next = this.originPoint[axis] + offset[axis];
11393
11330
  // Apply constraints
11394
- if (this.constraints && this.constraints[axis]) {
11395
- next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
11331
+ const constraints = this.constraints && this.constraints[axis];
11332
+ if (constraints) {
11333
+ next = applyConstraints(next, constraints, this.elastic[axis]);
11396
11334
  }
11397
11335
  axisValue.set(next);
11398
11336
  }
11399
11337
  resolveConstraints() {
11400
11338
  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;
11339
+ const { projection } = this.visualElement;
11340
+ const layout = projection && (projection.layout || projection.measure(false));
11405
11341
  const prevConstraints = this.constraints;
11406
11342
  if (isElementConstraints(dragConstraints)) {
11407
- if (!this.constraints) {
11343
+ /**
11344
+ * Ref constraints are measured with the layout. Scrolling a
11345
+ * `layoutScroll` ancestor since can move the element relative
11346
+ * to them without a layout measurement, so measure them again
11347
+ * (#2024). Otherwise keep them: they and the layout can both
11348
+ * have moved on the page since, e.g. when content above grows.
11349
+ */
11350
+ if (!this.constraints ||
11351
+ this.scroll !== measureScroll(projection)) {
11408
11352
  this.constraints = this.resolveRefConstraints();
11409
11353
  }
11410
11354
  }
@@ -11447,21 +11391,20 @@
11447
11391
  if (!projection || !projection.layout)
11448
11392
  return false;
11449
11393
  /**
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.
11394
+ * Refresh the scroll offsets of the root and `layoutScroll`
11395
+ * ancestors so the constraint's viewport box translates to the
11396
+ * layout's coordinates. The scroll captured when the layout was
11397
+ * measured can be stale if they've scrolled since — e.g. via the
11398
+ * browser restoring scroll on refresh, or an ancestor layout effect
11399
+ * running after this element's mount (#2829).
11458
11400
  */
11459
- if (projection.root) {
11460
- projection.root.scroll = undefined;
11461
- projection.root.updateScroll();
11462
- }
11401
+ this.scroll = measureScroll(projection);
11463
11402
  const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
11464
- let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
11403
+ /**
11404
+ * layoutBox removes the scroll of `layoutScroll` ancestors, so
11405
+ * remove it from the constraints box too.
11406
+ */
11407
+ let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, projection.removeElementScroll(constraintsBox));
11465
11408
  /**
11466
11409
  * If there's an onMeasureDragConstraints listener we call it and
11467
11410
  * if different constraints are returned, set constraints to that
@@ -11482,7 +11425,7 @@
11482
11425
  if (!shouldDrag(axis, dragAxes || drag, this.currentDirection)) {
11483
11426
  return;
11484
11427
  }
11485
- let transition = (constraints && constraints[axis]) || {};
11428
+ let transition = constraints[axis] || {};
11486
11429
  if (dragSnapToOrigin === true ||
11487
11430
  dragSnapToOrigin === axis)
11488
11431
  transition = { min: 0, max: 0 };
@@ -11588,7 +11531,7 @@
11588
11531
  this.visualElement.current.style.transform = transformTemplate
11589
11532
  ? transformTemplate({}, "")
11590
11533
  : "none";
11591
- projection.root && projection.root.updateScroll();
11534
+ projection.root?.updateScroll();
11592
11535
  projection.updateLayout();
11593
11536
  /**
11594
11537
  * Reset constraints so resolveConstraints() will recalculate them
@@ -11665,8 +11608,8 @@
11665
11608
  };
11666
11609
  const { projection } = this.visualElement;
11667
11610
  const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
11668
- if (projection && !projection.layout) {
11669
- projection.root && projection.root.updateScroll();
11611
+ if (!projection.layout) {
11612
+ projection.root?.updateScroll();
11670
11613
  projection.updateLayout();
11671
11614
  }
11672
11615
  frame.read(measureDragConstraints);
@@ -11701,22 +11644,14 @@
11701
11644
  stopResizeListener();
11702
11645
  stopPointerListener();
11703
11646
  stopMeasureLayoutListener();
11704
- stopLayoutUpdateListener && stopLayoutUpdateListener();
11705
- stopResizeObservers && stopResizeObservers();
11647
+ stopLayoutUpdateListener();
11648
+ stopResizeObservers?.();
11706
11649
  };
11707
11650
  }
11708
11651
  getProps() {
11709
11652
  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
- };
11653
+ const { dragElastic = defaultElastic, dragMomentum = true } = props;
11654
+ return { ...props, dragElastic, dragMomentum };
11720
11655
  }
11721
11656
  }
11722
11657
  /**
@@ -14051,6 +13986,12 @@
14051
13986
  }
14052
13987
 
14053
13988
  const reversePriorityOrder = [...variantPriorityOrder].reverse();
13989
+ /**
13990
+ * The transition a value uses when it leaves a state (#1725): the state's
13991
+ * own transition when `out: true`, or the `out` transition itself.
13992
+ * Value-specific keys are resolved by animateTarget.
13993
+ */
13994
+ const getOutTransition = (transition) => transition?.out === true ? transition : transition?.out;
14054
13995
  /**
14055
13996
  * Diffs the element's animation props (one layer per AnimationType, highest
14056
13997
  * priority wins each value) against the previous call and starts animations
@@ -14072,13 +14013,18 @@
14072
14013
  let wasReset = false;
14073
14014
  function animateChanges(changedActiveType) {
14074
14015
  const { props, parent, manuallyAnimateOnMount } = visualElement;
14016
+ const { initial } = props;
14075
14017
  const isMounting = isInitialRender || wasReset;
14076
14018
  const animations = [];
14019
+ const resolveInitial = (definition = initial) => (typeof definition !== "boolean" &&
14020
+ resolveVariant(visualElement, Array.isArray(definition) ? definition[0] : definition, visualElement.presenceContext?.custom)) ||
14021
+ undefined;
14077
14022
  /**
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.
14023
+ * Values removed from a type, mapped to the out transition of the
14024
+ * state they're leaving. A lower-priority type that defines them
14025
+ * animates them back, otherwise they fall back to a base target.
14080
14026
  */
14081
- const removedKeys = new Set();
14027
+ const removedKeys = new Map();
14082
14028
  /**
14083
14029
  * Values claimed by active, higher-priority types.
14084
14030
  */
@@ -14096,6 +14042,13 @@
14096
14042
  do
14097
14043
  source = source.props.inherit !== false && source.parent;
14098
14044
  while (source && !source.isControllingVariants);
14045
+ /**
14046
+ * Mounting leaves initial, so values use its out transition. Like the
14047
+ * other types, variant children inherit initial's label.
14048
+ */
14049
+ const initialOut = isMounting &&
14050
+ getOutTransition(resolveInitial(initial ?? (source && source.props.initial))
14051
+ ?.transition);
14099
14052
  reversePriorityOrder.forEach((type, i) => {
14100
14053
  const typeState = state[type];
14101
14054
  const ownProp = props[type];
@@ -14107,8 +14060,6 @@
14107
14060
  : undefined;
14108
14061
  const propIsVariant = isVariantLabel(prop);
14109
14062
  const activeDelta = type === changedActiveType ? typeState.isActive : null;
14110
- if (activeDelta === false)
14111
- removedVariantIndex = i;
14112
14063
  /**
14113
14064
  * Inherited labels are animated by the parent through
14114
14065
  * variantChildren, unless this element mounted after its parent.
@@ -14123,6 +14074,13 @@
14123
14074
  typeof prop === "boolean") {
14124
14075
  return;
14125
14076
  }
14077
+ /**
14078
+ * Set after the skip above, so deactivating a type that has nothing
14079
+ * to animate (e.g. whileTap with no prop) doesn't re-run the variants
14080
+ * below it.
14081
+ */
14082
+ if (activeDelta === false)
14083
+ removedVariantIndex = i;
14126
14084
  /**
14127
14085
  * Don't re-resolve a running exit: a changed custom would start
14128
14086
  * new value animations that stop the originals, leaving the exit
@@ -14142,6 +14100,7 @@
14142
14100
  let handledRemovedValues = false;
14143
14101
  const definitionList = Array.isArray(prop) ? prop : [prop];
14144
14102
  let resolvedValues = {};
14103
+ let out;
14145
14104
  if (activeDelta !== false) {
14146
14105
  for (const definition of definitionList) {
14147
14106
  const { transition, transitionEnd, ...target } = resolveVariant(visualElement, definition, getTypeCustom(visualElement, type)) || {};
@@ -14150,6 +14109,11 @@
14150
14109
  ...target,
14151
14110
  ...transitionEnd,
14152
14111
  };
14112
+ /**
14113
+ * One out transition per type. With a list of labels,
14114
+ * the last one that sets out applies to all its values.
14115
+ */
14116
+ out = getOutTransition(transition) || out;
14153
14117
  }
14154
14118
  }
14155
14119
  const { prevResolvedValues } = typeState;
@@ -14165,12 +14129,18 @@
14165
14129
  const valueHasChanged = Array.isArray(next) && Array.isArray(prev)
14166
14130
  ? !shallowCompare(next, prev) || variantDidChange
14167
14131
  : next !== prev;
14132
+ let outTransition;
14168
14133
  if (valueHasChanged && (next === undefined || next === null)) {
14169
- removedKeys.add(key);
14134
+ /**
14135
+ * The highest-priority state a value leaves wins.
14136
+ */
14137
+ removedKeys.has(key) ||
14138
+ removedKeys.set(key, typeState.prevOut);
14170
14139
  }
14171
14140
  else if (valueHasChanged ||
14172
14141
  (next !== undefined && removedKeys.has(key))) {
14173
14142
  shouldAnimateType = true;
14143
+ outTransition = removedKeys.get(key) || initialOut;
14174
14144
  if (removedKeys.delete(key))
14175
14145
  handledRemovedValues = true;
14176
14146
  typeState.needsAnimating[key] = true;
@@ -14181,9 +14151,11 @@
14181
14151
  else {
14182
14152
  typeState.protectedKeys[key] = true;
14183
14153
  }
14154
+ typeState.out[key] = outTransition;
14184
14155
  }
14185
14156
  typeState.prevProp = prop;
14186
14157
  typeState.prevResolvedValues = resolvedValues;
14158
+ typeState.prevOut = out;
14187
14159
  if (typeState.isActive) {
14188
14160
  encounteredKeys = { ...encounteredKeys, ...resolvedValues };
14189
14161
  }
@@ -14214,14 +14186,17 @@
14214
14186
  * longer defines a value it once did, it stays where it is.
14215
14187
  */
14216
14188
  if (removedKeys.size) {
14217
- const { initial } = props;
14218
14189
  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) => {
14190
+ const resolvedInitial = resolveInitial();
14191
+ let fallbackTransition = resolvedInitial?.transition;
14192
+ removedKeys.forEach((outTransition, key) => {
14193
+ if (outTransition) {
14194
+ fallbackTransition = {
14195
+ ...(fallbackTransition ||
14196
+ visualElement.getDefaultTransition()),
14197
+ [key]: outTransition,
14198
+ };
14199
+ }
14225
14200
  const motionValue = visualElement.getValue(key);
14226
14201
  if (motionValue)
14227
14202
  motionValue.liveStyle = true;
@@ -14252,6 +14227,9 @@
14252
14227
  visualElement.baseTarget[key] = motionValue?.get();
14253
14228
  }
14254
14229
  });
14230
+ if (fallbackTransition) {
14231
+ fallbackAnimation.transition = fallbackTransition;
14232
+ }
14255
14233
  animations.push({ animation: fallbackAnimation });
14256
14234
  }
14257
14235
  const blockAnimation = (isMounting && visualElement.blockInitialAnimation) ||
@@ -14311,6 +14289,7 @@
14311
14289
  protectedKeys: {},
14312
14290
  needsAnimating: {},
14313
14291
  prevResolvedValues: {},
14292
+ out: {},
14314
14293
  };
14315
14294
  }
14316
14295
  return state;
@@ -14429,6 +14408,18 @@
14429
14408
  return duration * (repeat + 1) + repeatDelay * repeat;
14430
14409
  }
14431
14410
 
14411
+ /**
14412
+ * The time of a label, or a label with an offset like "label+1" or
14413
+ * "label-0.5". An exact label name wins over an offset.
14414
+ */
14415
+ function calcLabelTime(position, labels) {
14416
+ if (labels.has(position))
14417
+ return labels.get(position);
14418
+ const [, name, offset] = position.match(/^(.+?)([+-][\d.]+)$/) || [];
14419
+ return labels.has(name)
14420
+ ? Math.max(0, labels.get(name) + parseFloat(offset))
14421
+ : undefined;
14422
+ }
14432
14423
  /**
14433
14424
  * Given a absolute or relative time definition and current/prev time state of the sequence,
14434
14425
  * calculate an absolute time for the next keyframes.
@@ -14447,7 +14438,7 @@
14447
14438
  return Math.max(0, prev + parseFloat(next.slice(1)));
14448
14439
  }
14449
14440
  else {
14450
- return labels.get(next) ?? current;
14441
+ return calcLabelTime(next, labels) ?? current;
14451
14442
  }
14452
14443
  }
14453
14444
 
@@ -14508,7 +14499,7 @@
14508
14499
 
14509
14500
  const defaultSegmentEasing = "easeInOut";
14510
14501
  const MAX_REPEAT = 20;
14511
- function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
14502
+ function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators, info) {
14512
14503
  const defaultDuration = defaultTransition.duration || 0.3;
14513
14504
  const animationDefinitions = new Map();
14514
14505
  const sequences = new Map();
@@ -14521,196 +14512,233 @@
14521
14512
  * Build the timeline by mapping over the sequence array and converting
14522
14513
  * the definitions into keyframes and offsets with absolute time values.
14523
14514
  * These will later get converted into relative offsets in a second pass.
14515
+ *
14516
+ * Nested sequences are resolved recursively. `base` is the absolute
14517
+ * start of the sequence being resolved, so numeric `at` values inside
14518
+ * a nested sequence are relative to where it starts. Returns the time
14519
+ * the sequence ends.
14524
14520
  */
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;
14555
- /**
14556
- * Resolve stagger() if defined.
14557
- */
14558
- const calculatedDelay = typeof delay === "function"
14559
- ? delay(elementIndex, numSubjects)
14560
- : delay;
14521
+ const resolveSequence = (sequence, base) => {
14522
+ let sequenceEnd = base;
14523
+ const resolveTime = (at) => typeof at === "number"
14524
+ ? base + at
14525
+ : calcNextTime(currentTime, at, prevTime, timeLabels);
14526
+ for (let i = 0; i < sequence.length; i++) {
14527
+ const segment = sequence[i];
14561
14528
  /**
14562
- * If this animation should and can use a spring, generate a spring easing function.
14529
+ * If this is a timeline label, mark it and skip the rest of this iteration.
14563
14530
  */
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);
14531
+ if (typeof segment === "string") {
14532
+ timeLabels.set(segment, currentTime);
14533
+ continue;
14534
+ }
14535
+ else if (!Array.isArray(segment)) {
14536
+ const start = segment.at === undefined
14537
+ ? currentTime
14538
+ : resolveTime(segment.at);
14539
+ if ("sequence" in segment) {
14540
+ /**
14541
+ * Resolve the nested sequence from its start. Afterwards
14542
+ * "<" points at its start and the next segment follows
14543
+ * its end, as if it were a single segment.
14544
+ */
14545
+ prevTime = currentTime = start;
14546
+ currentTime = resolveSequence(segment.sequence, start);
14547
+ prevTime = start;
14548
+ sequenceEnd = Math.max(sequenceEnd, currentTime);
14580
14549
  }
14581
- const springTransition = {
14582
- ...defaultTransition,
14583
- ...remainingTransition,
14584
- };
14585
- if (duration !== undefined) {
14586
- springTransition.duration = secondsToMilliseconds(duration);
14550
+ else {
14551
+ timeLabels.set(segment.name, start);
14587
14552
  }
14588
- const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
14589
- ease = springEasing.ease;
14590
- duration = springEasing.duration;
14553
+ continue;
14591
14554
  }
14592
- duration ?? (duration = defaultDuration);
14593
- const startTime = currentTime + calculatedDelay;
14555
+ let [subject, keyframes, transition = {}] = segment;
14594
14556
  /**
14595
- * If there's only one time offset of 0, fill in a second with length 1
14557
+ * If a relative or absolute time value has been specified we need to resolve
14558
+ * it in relation to the currentTime.
14596
14559
  */
14597
- if (times.length === 1 && times[0] === 0) {
14598
- times[1] = 1;
14560
+ if (transition.at !== undefined) {
14561
+ currentTime = resolveTime(transition.at);
14599
14562
  }
14600
14563
  /**
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.
14564
+ * Keep track of the maximum duration in this definition. This will be
14565
+ * applied to currentTime once the definition has been parsed.
14609
14566
  */
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.
14616
- */
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) {
14567
+ let maxDuration = 0;
14568
+ const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
14569
+ const valueKeyframesAsList = keyframesAsList(valueKeyframes);
14570
+ const { delay = 0, times = defaultOffset$1(valueKeyframesAsList), type = defaultTransition.type || "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
14571
+ let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
14621
14572
  /**
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.
14573
+ * Resolve stagger() if defined.
14625
14574
  */
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];
14575
+ const calculatedDelay = typeof delay === "function"
14576
+ ? delay(elementIndex, numSubjects)
14577
+ : delay;
14632
14578
  /**
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).
14579
+ * If this animation should and can use a spring, generate a spring easing function.
14640
14580
  */
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);
14581
+ const numKeyframes = valueKeyframesAsList.length;
14582
+ const createGenerator = isGenerator(type)
14583
+ ? type
14584
+ : generators?.[type || "keyframes"];
14585
+ if (numKeyframes <= 2 && createGenerator) {
14586
+ /**
14587
+ * As we're creating an easing function from a spring,
14588
+ * ideally we want to generate it using the real distance
14589
+ * between the two keyframes. However this isn't always
14590
+ * possible - in these situations we use 0-100.
14591
+ */
14592
+ let absoluteDelta = 100;
14593
+ if (numKeyframes === 2 &&
14594
+ isNumberKeyframesArray(valueKeyframesAsList)) {
14595
+ const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
14596
+ absoluteDelta = Math.abs(delta);
14597
+ }
14598
+ const springTransition = {
14599
+ ...defaultTransition,
14600
+ ...remainingTransition,
14601
+ };
14602
+ if (duration !== undefined) {
14603
+ springTransition.duration =
14604
+ secondsToMilliseconds(duration);
14652
14605
  }
14606
+ const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
14607
+ ease = springEasing.ease;
14608
+ duration = springEasing.duration;
14609
+ }
14610
+ duration ?? (duration = defaultDuration);
14611
+ const startTime = currentTime + calculatedDelay;
14612
+ /**
14613
+ * If there's only one time offset of 0, fill in a second with length 1
14614
+ */
14615
+ if (times.length === 1 && times[0] === 0) {
14616
+ times[1] = 1;
14653
14617
  }
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);
14618
+ /**
14619
+ * Fill out if offset if fewer offsets than keyframes
14620
+ */
14621
+ const remainder = times.length - valueKeyframesAsList.length;
14622
+ remainder > 0 && fillOffset(times, remainder);
14623
+ /**
14624
+ * If only one value has been set, ie [1], push a null to the start of
14625
+ * the keyframe array. This will let us mark a keyframe at this point
14626
+ * that will later be hydrated with the previous value.
14627
+ */
14628
+ valueKeyframesAsList.length === 1 &&
14629
+ valueKeyframesAsList.unshift(null);
14630
+ /**
14631
+ * Segments can't express `repeat: Infinity` or very large
14632
+ * counts — they'd leave dead time after the segment or
14633
+ * explode the keyframe array. Ignore with a warning.
14634
+ */
14635
+ if (repeat) {
14636
+ typeof process !== "undefined" &&
14637
+ "development" !== "production" &&
14638
+ 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.`);
14639
+ }
14640
+ if (repeat && repeat < MAX_REPEAT) {
14641
+ /**
14642
+ * Express repeatDelay in units of a single iteration's duration
14643
+ * so it can be added to the per-iteration time offsets below
14644
+ * before they're normalized to 0-1.
14645
+ */
14646
+ const repeatDelayUnits = duration > 0 ? repeatDelay / duration : 0;
14647
+ duration = calculateRepeatDuration(duration, repeat, repeatDelay);
14648
+ const originalKeyframes = [...valueKeyframesAsList];
14649
+ const originalTimes = [...times];
14650
+ ease = Array.isArray(ease) ? [...ease] : [ease];
14651
+ const originalEase = [...ease];
14661
14652
  /**
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.
14653
+ * For reverse/mirror, alternate iterations play the segment
14654
+ * backwards. mirror matches JSAnimation's mirroredGenerator:
14655
+ * reversed keyframes, easings unchanged. reverse matches
14656
+ * JSAnimation's iterationProgress = 1 - p: reversed
14657
+ * keyframes, easing array reversed AND each function easing
14658
+ * mapped through reverseEasing (string easings unchanged —
14659
+ * they're resolved later by the keyframes engine).
14665
14660
  */
14666
- if (repeatDelayUnits > 0) {
14667
- valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
14668
- times.push(iterStartOffset);
14669
- ease.push("linear");
14661
+ const isFlipping = repeatType === "reverse" || repeatType === "mirror";
14662
+ let flippedKeyframes = originalKeyframes;
14663
+ let flippedEases = originalEase;
14664
+ if (isFlipping) {
14665
+ flippedKeyframes = [...originalKeyframes].reverse();
14666
+ if (repeatType === "reverse") {
14667
+ flippedEases = [...originalEase]
14668
+ .reverse()
14669
+ .map((e) => typeof e === "function"
14670
+ ? reverseEasing(e)
14671
+ : e);
14672
+ }
14670
14673
  }
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));
14674
+ for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
14675
+ const isFlipped = isFlipping && repeatIndex % 2 === 0;
14676
+ const iterKeyframes = isFlipped
14677
+ ? flippedKeyframes
14678
+ : originalKeyframes;
14679
+ const iterEase = isFlipped ? flippedEases : originalEase;
14680
+ const iterStartOffset = (repeatIndex + 1) * (1 + repeatDelayUnits);
14681
+ /**
14682
+ * If repeatDelay is set, hold the previous iteration's
14683
+ * final value through the delay by inserting a keyframe
14684
+ * at the moment the next iteration begins.
14685
+ */
14686
+ if (repeatDelayUnits > 0) {
14687
+ valueKeyframesAsList.push(valueKeyframesAsList[valueKeyframesAsList.length - 1]);
14688
+ times.push(iterStartOffset);
14689
+ ease.push("linear");
14690
+ }
14691
+ valueKeyframesAsList.push(...iterKeyframes);
14692
+ for (let keyframeIndex = 0; keyframeIndex < iterKeyframes.length; keyframeIndex++) {
14693
+ times.push(originalTimes[keyframeIndex] + iterStartOffset);
14694
+ ease.push(keyframeIndex === 0
14695
+ ? "linear"
14696
+ : getEasingForSegment(iterEase, keyframeIndex - 1));
14697
+ }
14677
14698
  }
14699
+ normalizeTimes(times, repeat, repeatDelayUnits);
14678
14700
  }
14679
- normalizeTimes(times, repeat, repeatDelayUnits);
14701
+ const targetTime = startTime + duration;
14702
+ /**
14703
+ * Add keyframes, mapping offsets to absolute time.
14704
+ */
14705
+ addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
14706
+ maxDuration = Math.max(calculatedDelay + duration, maxDuration);
14707
+ totalDuration = Math.max(targetTime, totalDuration);
14708
+ };
14709
+ if (isMotionValue(subject)) {
14710
+ const subjectSequence = getSubjectSequence(subject, sequences);
14711
+ resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
14680
14712
  }
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++) {
14713
+ else {
14714
+ const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
14715
+ const numSubjects = subjects.length;
14700
14716
  /**
14701
- * Cast necessary, but we know these are of this type
14717
+ * For every element in this segment, process the defined values.
14702
14718
  */
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);
14719
+ for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
14720
+ /**
14721
+ * Cast necessary, but we know these are of this type
14722
+ */
14723
+ keyframes = keyframes;
14724
+ transition = transition;
14725
+ const thisSubject = subjects[subjectIndex];
14726
+ const subjectSequence = getSubjectSequence(thisSubject, sequences);
14727
+ for (const key in keyframes) {
14728
+ resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
14729
+ }
14709
14730
  }
14710
14731
  }
14732
+ prevTime = currentTime;
14733
+ currentTime += maxDuration;
14734
+ sequenceEnd = Math.max(sequenceEnd, currentTime);
14711
14735
  }
14712
- prevTime = currentTime;
14713
- currentTime += maxDuration;
14736
+ return sequenceEnd;
14737
+ };
14738
+ resolveSequence(sequence, 0);
14739
+ if (info) {
14740
+ info.duration = totalDuration;
14741
+ info.labels = timeLabels;
14714
14742
  }
14715
14743
  /**
14716
14744
  * For every element and value combination create a new animation.
@@ -14858,7 +14886,7 @@
14858
14886
  return animations;
14859
14887
  }
14860
14888
 
14861
- function animateSequence(sequence, options, scope) {
14889
+ function animateSequence(sequence, options, scope, info) {
14862
14890
  const animations = [];
14863
14891
  /**
14864
14892
  * Pre-process: replace function segments with MotionValue segments,
@@ -14881,13 +14909,515 @@
14881
14909
  }
14882
14910
  return segment;
14883
14911
  });
14884
- const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring });
14912
+ const animationDefinitions = createAnimationsFromSequence(processedSequence, options, scope, { spring }, info);
14885
14913
  animationDefinitions.forEach(({ keyframes, transition }, subject) => {
14886
14914
  animations.push(...animateSubject(subject, keyframes, transition));
14887
14915
  });
14888
14916
  return animations;
14889
14917
  }
14890
14918
 
14919
+ let markerId = 0;
14920
+ /**
14921
+ * Copy a segment with a new `at`, adding a transition if it has none.
14922
+ */
14923
+ function withAt(segment, at) {
14924
+ const copy = [...segment];
14925
+ const i = typeof copy[0] === "function" ? Math.max(1, copy.length - 1) : 2;
14926
+ copy[i] = { ...copy[i], at };
14927
+ return copy;
14928
+ }
14929
+ const label = (name, at) => at === undefined ? name : { name, at };
14930
+ /**
14931
+ * Rewind to the start and stop, so every value is back at the value it
14932
+ * started from and the next build reads those values as its origin.
14933
+ */
14934
+ function rewind(animation) {
14935
+ animation.pause();
14936
+ animation.time = 0;
14937
+ animation.stop();
14938
+ }
14939
+ /**
14940
+ * A sequence that can be changed after it's created. Segments, labels,
14941
+ * callbacks, pauses and other timelines can be added, moved and removed
14942
+ * at any point, including while it plays.
14943
+ *
14944
+ * Changes are batched and applied once per microtask. Each change
14945
+ * rebuilds the underlying animations from the timeline's starting
14946
+ * values, then restores the playhead, speed and play state, so changes
14947
+ * made while playing take effect without a jump.
14948
+ */
14949
+ class Timeline {
14950
+ /**
14951
+ * `animations` are already running animations of `items`, as
14952
+ * animate() makes them. The timeline plays these until it's first
14953
+ * changed, so it costs nothing over the animations themselves until
14954
+ * then.
14955
+ */
14956
+ constructor(items = [], options = {}, animations) {
14957
+ this.options = options;
14958
+ this.entries = [];
14959
+ /**
14960
+ * The repeat the playhead is in, to detect when it starts another.
14961
+ */
14962
+ this.repeatIndex = 0;
14963
+ this.prevTime = 0;
14964
+ this.markers = [];
14965
+ this.labels = new Map();
14966
+ this.totalDuration = 0;
14967
+ this.isDirty = false;
14968
+ this.isDone = false;
14969
+ this.isRebuilding = false;
14970
+ this.playbackSpeed = 1;
14971
+ /**
14972
+ * True while this plays the animations it was created with, before
14973
+ * any change compiles it as a sequence.
14974
+ */
14975
+ this.isWrapped = false;
14976
+ this.stop = () => {
14977
+ this.flush();
14978
+ this.group?.stop();
14979
+ };
14980
+ this.isPaused = options.autoplay === false;
14981
+ this.newFinished();
14982
+ items.forEach((item) => this.add(item));
14983
+ if (animations) {
14984
+ this.isDirty = false;
14985
+ this.isWrapped = true;
14986
+ this.group = new GroupAnimationWithThen(animations);
14987
+ this.watch();
14988
+ }
14989
+ else {
14990
+ /**
14991
+ * Build now, like animate(), so selectors resolve while a
14992
+ * scope is attached. Later changes are batched.
14993
+ */
14994
+ this.invalidate();
14995
+ this.flush();
14996
+ }
14997
+ /**
14998
+ * Register with the scope (for instance from useAnimate()) so it
14999
+ * stops when the scope is torn down.
15000
+ */
15001
+ const { scope } = options;
15002
+ if (scope) {
15003
+ scope.animations.push(this);
15004
+ this.finished.then(() => removeItem(scope.animations, this));
15005
+ }
15006
+ }
15007
+ /**
15008
+ * Add a segment, label or timeline. `at` overrides any `at` already
15009
+ * on the segment. The item itself is the handle for `move()` and
15010
+ * `remove()`.
15011
+ */
15012
+ add(item, at) {
15013
+ if (item instanceof Timeline)
15014
+ item.adopt(this);
15015
+ return this.push({ item, at });
15016
+ }
15017
+ /**
15018
+ * Fire a callback when the playhead crosses `at`, in either direction.
15019
+ * It receives the direction: 1 forwards, -1 backwards.
15020
+ */
15021
+ call(callback, at) {
15022
+ return this.push({
15023
+ item: callback,
15024
+ at,
15025
+ marker: { name: "_m" + markerId++, time: 0, callback },
15026
+ });
15027
+ }
15028
+ /**
15029
+ * Pause when the playhead reaches `at`.
15030
+ */
15031
+ addPause(at, callback) {
15032
+ return this.push({
15033
+ item: callback || {},
15034
+ at,
15035
+ marker: { name: "_m" + markerId++, time: 0, callback, pause: true },
15036
+ });
15037
+ }
15038
+ /**
15039
+ * Change where an item starts.
15040
+ */
15041
+ move(item, at) {
15042
+ this.entries.forEach((entry) => {
15043
+ if (entry.item === item)
15044
+ entry.at = at;
15045
+ });
15046
+ return this.invalidate();
15047
+ }
15048
+ /**
15049
+ * Remove a segment, label, callback or timeline.
15050
+ */
15051
+ remove(item) {
15052
+ if (item instanceof Timeline)
15053
+ item.parent = undefined;
15054
+ this.entries = this.entries.filter((entry) => entry.item !== item);
15055
+ return this.invalidate();
15056
+ }
15057
+ clear() {
15058
+ this.entries = [];
15059
+ return this.invalidate();
15060
+ }
15061
+ /**
15062
+ * The time of a label in seconds, or `undefined` if there's no label
15063
+ * with this name.
15064
+ */
15065
+ getLabelTime(name) {
15066
+ this.flush();
15067
+ return this.labels.get(name);
15068
+ }
15069
+ /**
15070
+ * Jump to a time in seconds or a label.
15071
+ */
15072
+ seek(position) {
15073
+ this.time = this.resolve(position);
15074
+ return this;
15075
+ }
15076
+ /**
15077
+ * Play forwards or backwards to a time or label, then pause there.
15078
+ */
15079
+ playTo(position) {
15080
+ const target = this.resolve(position);
15081
+ const speed = Math.abs(this.speed);
15082
+ this.speed = target < this.time ? -speed : speed;
15083
+ this.stopAt = target;
15084
+ this.clock || this.invalidate();
15085
+ this.play();
15086
+ return this;
15087
+ }
15088
+ reverse() {
15089
+ this.speed = -this.speed;
15090
+ this.play();
15091
+ return this;
15092
+ }
15093
+ get progress() {
15094
+ return this.duration ? this.time / this.duration : 0;
15095
+ }
15096
+ set progress(progress) {
15097
+ this.time = progress * this.duration;
15098
+ }
15099
+ get time() {
15100
+ return this.active()?.time ?? 0;
15101
+ }
15102
+ set time(time) {
15103
+ this.flush();
15104
+ this.prevTime = time;
15105
+ this.group && (this.group.time = time);
15106
+ this.repeatIndex = this.currentRepeat();
15107
+ }
15108
+ get speed() {
15109
+ return this.playbackSpeed;
15110
+ }
15111
+ set speed(speed) {
15112
+ this.playbackSpeed = speed;
15113
+ this.group && (this.group.speed = speed);
15114
+ }
15115
+ get duration() {
15116
+ this.flush();
15117
+ return this.isWrapped ? this.group.duration : this.totalDuration;
15118
+ }
15119
+ get iterationDuration() {
15120
+ this.flush();
15121
+ return this.isWrapped
15122
+ ? this.group.iterationDuration
15123
+ : this.totalDuration;
15124
+ }
15125
+ get state() {
15126
+ return this.active()?.state ?? "idle";
15127
+ }
15128
+ get startTime() {
15129
+ return this.active()?.startTime ?? null;
15130
+ }
15131
+ play() {
15132
+ this.flush();
15133
+ this.isPaused = false;
15134
+ this.group?.play();
15135
+ /**
15136
+ * Watch after playing, so it waits for the restarted animations
15137
+ * rather than the ones that already finished.
15138
+ */
15139
+ if (this.isDone) {
15140
+ this.newFinished();
15141
+ this.watch();
15142
+ }
15143
+ }
15144
+ pause() {
15145
+ this.flush();
15146
+ this.isPaused = true;
15147
+ this.group?.pause();
15148
+ }
15149
+ cancel() {
15150
+ this.flush();
15151
+ this.group?.cancel();
15152
+ }
15153
+ complete() {
15154
+ this.flush();
15155
+ this.group?.complete();
15156
+ }
15157
+ attachTimeline(timeline) {
15158
+ this.flush();
15159
+ this.scrollTimeline = timeline;
15160
+ this.detachScroll = this.group?.attachTimeline(timeline);
15161
+ return () => {
15162
+ this.scrollTimeline = undefined;
15163
+ this.detachScroll?.();
15164
+ };
15165
+ }
15166
+ then(onResolve, onReject) {
15167
+ return this.finished.then(onResolve, onReject);
15168
+ }
15169
+ /**
15170
+ * The current animations, if any are running. Skipped animations
15171
+ * (skipAnimations, reduced motion) can leave the group empty.
15172
+ */
15173
+ active() {
15174
+ this.flush();
15175
+ return this.group?.animations.length ? this.group : undefined;
15176
+ }
15177
+ push(entry) {
15178
+ this.entries.push(entry);
15179
+ return this.invalidate();
15180
+ }
15181
+ /**
15182
+ * Schedule a rebuild. A nested timeline passes changes up, as it's
15183
+ * built as part of its parent.
15184
+ */
15185
+ invalidate() {
15186
+ if (this.parent) {
15187
+ this.parent.invalidate();
15188
+ }
15189
+ else if (!this.isDirty) {
15190
+ this.isDirty = true;
15191
+ queueMicrotask(() => this.flush());
15192
+ }
15193
+ return this;
15194
+ }
15195
+ /**
15196
+ * Apply pending changes now, so reads straight after a change
15197
+ * reflect it.
15198
+ */
15199
+ flush() {
15200
+ this.isDirty && !this.parent && this.build();
15201
+ }
15202
+ adopt(parent) {
15203
+ if (this.group) {
15204
+ rewind(this.group);
15205
+ this.group = undefined;
15206
+ }
15207
+ this.isDirty = false;
15208
+ this.parent = parent;
15209
+ }
15210
+ build() {
15211
+ this.isDirty = false;
15212
+ this.isWrapped = false;
15213
+ const prev = this.group;
15214
+ let time = 0;
15215
+ if (prev) {
15216
+ this.isRebuilding = true;
15217
+ if (this.scrollTimeline) {
15218
+ /**
15219
+ * Scroll-linked animations can't be seeked in seconds.
15220
+ * Cancelling also returns them to their starting values,
15221
+ * and the new build is attached to scroll instead.
15222
+ */
15223
+ this.detachScroll?.();
15224
+ prev.cancel();
15225
+ }
15226
+ else {
15227
+ time = prev.animations.length ? prev.time : 0;
15228
+ rewind(prev);
15229
+ }
15230
+ this.isRebuilding = false;
15231
+ }
15232
+ const { defaultTransition, onComplete, onUpdate, onRepeat, onReverseComplete, autoplay, scope, ...options } = this.options;
15233
+ /**
15234
+ * Inherit MotionConfig settings from a useAnimate() scope.
15235
+ */
15236
+ const playback = {};
15237
+ if (scope?.reduceMotion !== undefined) {
15238
+ playback.reduceMotion = scope.reduceMotion;
15239
+ }
15240
+ if (scope?.skipAnimations !== undefined) {
15241
+ playback.skipAnimations = scope.skipAnimations;
15242
+ }
15243
+ Object.assign(playback, options);
15244
+ const markers = new Map();
15245
+ const info = {};
15246
+ const animations = animateSequence(this.toSequence(markers), { defaultTransition, ...playback }, scope, info);
15247
+ const labels = info.labels;
15248
+ let duration = playback.duration ?? info.duration;
15249
+ /**
15250
+ * A `duration` option stretches every animation to fit, so scale
15251
+ * label and marker times to match.
15252
+ */
15253
+ const scale = info.duration ? duration / info.duration : 1;
15254
+ labels.forEach((time, name) => labels.set(name, time * scale));
15255
+ /**
15256
+ * Markers are resolved as labels. Move them out of the label map
15257
+ * and extend the duration to reach any marker past the end.
15258
+ */
15259
+ this.markers = [];
15260
+ markers.forEach((marker, name) => {
15261
+ marker.time = labels.get(name);
15262
+ labels.delete(name);
15263
+ duration = Math.max(duration, marker.time);
15264
+ this.markers.push(marker);
15265
+ });
15266
+ this.markers.sort((a, b) => a.time - b.time);
15267
+ this.labels = labels;
15268
+ this.totalDuration = duration;
15269
+ /**
15270
+ * Callbacks, pauses and playTo() watch a linear clock. Without
15271
+ * them, the timeline is only its animations.
15272
+ */
15273
+ this.clock = this.clockAnimation = undefined;
15274
+ if (this.markers.length ||
15275
+ this.stopAt !== undefined ||
15276
+ onUpdate ||
15277
+ onRepeat) {
15278
+ const clock = (this.clock = motionValue(0));
15279
+ clock.on("change", (t) => clock === this.clock && this.onClock(t));
15280
+ animations.unshift((this.clockAnimation = animateSingleValue(clock, [0, duration], {
15281
+ ...playback,
15282
+ duration,
15283
+ ease: "linear",
15284
+ })));
15285
+ }
15286
+ const group = (this.group = new GroupAnimationWithThen(animations));
15287
+ time = Math.min(time, duration);
15288
+ if (this.isDone && time < duration)
15289
+ this.newFinished();
15290
+ if (this.scrollTimeline) {
15291
+ this.detachScroll = group.attachTimeline(this.scrollTimeline);
15292
+ }
15293
+ else {
15294
+ group.speed = this.playbackSpeed;
15295
+ if (this.isPaused)
15296
+ group.pause();
15297
+ if (time)
15298
+ group.time = time;
15299
+ }
15300
+ this.repeatIndex = this.currentRepeat();
15301
+ /**
15302
+ * Treat a start from 0 as coming from just before it, so markers
15303
+ * at 0 fire.
15304
+ */
15305
+ this.prevTime = time || -1;
15306
+ this.watch();
15307
+ }
15308
+ toSequence(markers) {
15309
+ return this.entries.map(({ item, at, marker }) => {
15310
+ if (marker) {
15311
+ markers.set(marker.name, marker);
15312
+ return label(marker.name, at);
15313
+ }
15314
+ if (item instanceof Timeline) {
15315
+ return { sequence: item.toSequence(markers), at };
15316
+ }
15317
+ if (typeof item === "string")
15318
+ return label(item, at);
15319
+ if (at === undefined)
15320
+ return item;
15321
+ return Array.isArray(item)
15322
+ ? withAt(item, at)
15323
+ : { ...item, at };
15324
+ });
15325
+ }
15326
+ resolve(position) {
15327
+ this.flush();
15328
+ return typeof position === "number"
15329
+ ? position
15330
+ : calcLabelTime(position, this.labels) ?? this.time;
15331
+ }
15332
+ /**
15333
+ * The repeat the clock is in, from its time across all repeats.
15334
+ */
15335
+ currentRepeat() {
15336
+ const { repeat = 0, repeatDelay = 0, delay = 0 } = this.options;
15337
+ const cycle = this.totalDuration + repeatDelay;
15338
+ const time = (this.clockAnimation?.time ?? 0) - delay;
15339
+ return cycle && time > 0
15340
+ ? Math.min(repeat, Math.floor(time / cycle))
15341
+ : 0;
15342
+ }
15343
+ onClock(time) {
15344
+ let prevTime = this.prevTime;
15345
+ this.prevTime = time;
15346
+ if (this.isRebuilding)
15347
+ return;
15348
+ const { onUpdate, onRepeat, repeatType = "loop" } = this.options;
15349
+ onUpdate?.(time);
15350
+ const repeatIndex = this.currentRepeat();
15351
+ if (repeatIndex !== this.repeatIndex) {
15352
+ this.repeatIndex = repeatIndex;
15353
+ onRepeat?.();
15354
+ /**
15355
+ * A loop jumps the playhead back to the start, or to the
15356
+ * end when playing backwards. Finish crossing the repeat
15357
+ * that ended, then cross the new one from its edge.
15358
+ */
15359
+ if (repeatType === "loop") {
15360
+ const isForward = this.speed > 0;
15361
+ if (this.cross(prevTime, isForward ? this.totalDuration : 0)) {
15362
+ return;
15363
+ }
15364
+ prevTime = isForward ? -1 : Infinity;
15365
+ }
15366
+ }
15367
+ this.cross(prevTime, time);
15368
+ }
15369
+ /**
15370
+ * Fire markers between two playhead times. Returns `true` if it
15371
+ * paused.
15372
+ */
15373
+ cross(from, to) {
15374
+ if (from === to)
15375
+ return;
15376
+ const isForward = to > from;
15377
+ const crossed = (t) => isForward ? t > from && t <= to : t < from && t >= to;
15378
+ const markers = isForward ? this.markers : [...this.markers].reverse();
15379
+ for (const marker of markers) {
15380
+ if (!crossed(marker.time))
15381
+ continue;
15382
+ marker.callback?.(isForward ? 1 : -1);
15383
+ if (marker.pause)
15384
+ return this.pauseAt(marker.time);
15385
+ }
15386
+ if (this.stopAt !== undefined && crossed(this.stopAt)) {
15387
+ this.pauseAt(this.stopAt);
15388
+ this.stopAt = undefined;
15389
+ return true;
15390
+ }
15391
+ }
15392
+ pauseAt(time) {
15393
+ this.pause();
15394
+ this.time = time;
15395
+ return true;
15396
+ }
15397
+ watch() {
15398
+ const group = this.group;
15399
+ group.finished.then(() => {
15400
+ if (group !== this.group || this.isDone)
15401
+ return;
15402
+ this.isDone = true;
15403
+ const { onComplete, onReverseComplete } = this.options;
15404
+ /**
15405
+ * Without onReverseComplete, onComplete fires in either
15406
+ * direction, as it does for a single animation.
15407
+ */
15408
+ const callback = (this.speed < 0 && onReverseComplete) || onComplete;
15409
+ callback?.();
15410
+ this.resolveFinished();
15411
+ });
15412
+ }
15413
+ newFinished() {
15414
+ this.isDone = false;
15415
+ this.finished = new Promise((resolve) => {
15416
+ this.resolveFinished = resolve;
15417
+ });
15418
+ }
15419
+ }
15420
+
14891
15421
  function isSequence(value) {
14892
15422
  return Array.isArray(value) && value.some(Array.isArray);
14893
15423
  }
@@ -14901,8 +15431,6 @@
14901
15431
  * Implementation
14902
15432
  */
14903
15433
  function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
14904
- let animations = [];
14905
- let animationOnComplete;
14906
15434
  const motionScope = activeScope.current;
14907
15435
  const scopeTransition = motionScope?.transition;
14908
15436
  const inherited = {};
@@ -14910,45 +15438,53 @@
14910
15438
  inherited.reduceMotion = reduceMotion;
14911
15439
  if (skipAnimations !== undefined)
14912
15440
  inherited.skipAnimations = skipAnimations;
15441
+ let timeline;
14913
15442
  if (isSequence(subjectOrSequence)) {
14914
- const { onComplete, ...sequenceOptions } = optionsOrKeyframes || {};
14915
- if (typeof onComplete === "function") {
14916
- animationOnComplete = onComplete;
14917
- }
15443
+ const sequenceOptions = {
15444
+ ...inherited,
15445
+ ...optionsOrKeyframes,
15446
+ scope,
15447
+ };
14918
15448
  if (scopeTransition) {
14919
15449
  sequenceOptions.defaultTransition = {
14920
15450
  ...scopeTransition,
14921
15451
  ...sequenceOptions.defaultTransition,
14922
15452
  };
14923
15453
  }
14924
- animations = animateSequence(subjectOrSequence, { ...inherited, ...sequenceOptions }, scope);
15454
+ timeline = new Timeline(subjectOrSequence, sequenceOptions);
14925
15455
  }
14926
15456
  else {
14927
15457
  // Extract top-level onComplete so it doesn't get applied per-value
14928
15458
  const { onComplete, ...rest } = options || {};
14929
- if (typeof onComplete === "function") {
14930
- animationOnComplete = onComplete;
14931
- }
14932
- animations = animateSubject(subjectOrSequence, optionsOrKeyframes, {
15459
+ const transition = {
14933
15460
  ...inherited,
14934
15461
  ...scopeTransition,
14935
15462
  ...rest,
14936
- }, scope);
14937
- }
14938
- const animation = new GroupAnimationWithThen(animations);
14939
- if (animationOnComplete) {
14940
- animation.finished.then(animationOnComplete);
15463
+ };
15464
+ /**
15465
+ * Play the animations directly, and only compile them as a
15466
+ * sequence if the timeline is changed. Values animated from a
15467
+ * number are animated via their onUpdate callback instead.
15468
+ */
15469
+ const { onUpdate } = rest;
15470
+ const segment = (typeof subjectOrSequence === "number" ||
15471
+ (typeof subjectOrSequence === "string" &&
15472
+ (typeof optionsOrKeyframes === "string" ||
15473
+ Array.isArray(optionsOrKeyframes)))
15474
+ ? [
15475
+ onUpdate || (() => { }),
15476
+ Array.isArray(optionsOrKeyframes)
15477
+ ? optionsOrKeyframes
15478
+ : [subjectOrSequence, optionsOrKeyframes],
15479
+ { ...transition, onUpdate: undefined },
15480
+ ]
15481
+ : [subjectOrSequence, optionsOrKeyframes, transition]);
15482
+ timeline = new Timeline([segment], { ...inherited, onComplete, scope }, animateSubject(subjectOrSequence, optionsOrKeyframes, transition, scope));
14941
15483
  }
14942
15484
  if (motionScope) {
14943
- animation.finished.then(motionScope.add(() => animation.stop()));
15485
+ timeline.finished.then(motionScope.add(() => timeline.stop()));
14944
15486
  }
14945
- if (scope) {
14946
- scope.animations.push(animation);
14947
- animation.finished.then(() => {
14948
- removeItem(scope.animations, animation);
14949
- });
14950
- }
14951
- return animation;
15487
+ return timeline;
14952
15488
  }
14953
15489
  return scopedAnimate;
14954
15490
  }
@@ -14971,7 +15507,9 @@
14971
15507
  }
14972
15508
  const elements = resolveElements(elementOrSelector, scope);
14973
15509
  const numElements = elements.length;
14974
- exports.invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
15510
+ typeof process !== "undefined" &&
15511
+ "development" !== "production" &&
15512
+ exports.invariant(Boolean(numElements), "No valid elements provided.", "no-valid-elements");
14975
15513
  /**
14976
15514
  * WAAPI doesn't support interrupting animations.
14977
15515
  *
@@ -15280,22 +15818,26 @@
15280
15818
  const p = (i + progress(at(i), at(i + 1), v)) / n;
15281
15819
  return reverse ? 1 - p : p;
15282
15820
  }
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);
15821
+ /**
15822
+ * Resolves offsets on both axes, as useScroll reports the target's progress
15823
+ * on each (#2546). `axis` only picks which progress scroll() reports.
15824
+ */
15825
+ function resolveOffsets(info, { offset: offsetDefinition = ScrollOffset.All }) {
15826
+ for (const axis in axisKeys) {
15827
+ const axisInfo = info[axis];
15828
+ /**
15829
+ * Reset the length of the resolved offset array rather than creating a new one.
15830
+ */
15831
+ axisInfo.offset.length = 0;
15832
+ /**
15833
+ * Populate the offset array by resolving the user's offset definition into
15834
+ * a list of pixel scroll offsets.
15835
+ */
15836
+ for (let i = 0; i < offsetDefinition.length; i++) {
15837
+ axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
15838
+ }
15839
+ axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
15297
15840
  }
15298
- axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
15299
15841
  }
15300
15842
 
15301
15843
  function calcInset(element, container) {
@@ -15801,6 +16343,7 @@
15801
16343
  exports.buildSVGAttrs = buildSVGAttrs;
15802
16344
  exports.buildSVGPath = buildSVGPath;
15803
16345
  exports.buildTransform = buildTransform$1;
16346
+ exports.buildTransformer = buildTransformer;
15804
16347
  exports.calcAxisDelta = calcAxisDelta;
15805
16348
  exports.calcBoxDelta = calcBoxDelta;
15806
16349
  exports.calcChildStagger = calcChildStagger;