motion 12.40.0 → 12.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,7 +79,7 @@
79
79
  }
80
80
 
81
81
  /*#__NO_SIDE_EFFECTS__*/
82
- const noop$1 = (any) => any;
82
+ const noop = (any) => any;
83
83
 
84
84
  /**
85
85
  * Pipe
@@ -216,7 +216,7 @@
216
216
  function cubicBezier(mX1, mY1, mX2, mY2) {
217
217
  // If this is a linear gradient, return linear easing
218
218
  if (mX1 === mY1 && mX2 === mY2)
219
- return noop$1;
219
+ return noop;
220
220
  const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
221
221
  // If animation is at start/end, return t without easing
222
222
  return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
@@ -277,7 +277,7 @@
277
277
  const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
278
278
 
279
279
  const easingLookup = {
280
- linear: noop$1,
280
+ linear: noop,
281
281
  easeIn,
282
282
  easeInOut,
283
283
  easeOut,
@@ -318,12 +318,7 @@
318
318
  "postRender", // Compute
319
319
  ];
320
320
 
321
- const statsBuffer = {
322
- value: null,
323
- addProjectionMetrics: null,
324
- };
325
-
326
- function createRenderStep(runNextFrame, stepName) {
321
+ function createRenderStep(runNextFrame) {
327
322
  /**
328
323
  * We create and reuse two queues, one to queue jobs for the current frame
329
324
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -345,13 +340,11 @@
345
340
  timestamp: 0.0,
346
341
  isProcessing: false,
347
342
  };
348
- let numCalls = 0;
349
343
  function triggerCallback(callback) {
350
344
  if (toKeepAlive.has(callback)) {
351
345
  step.schedule(callback);
352
346
  runNextFrame();
353
347
  }
354
- numCalls++;
355
348
  callback(latestFrameData);
356
349
  }
357
350
  const step = {
@@ -394,13 +387,6 @@
394
387
  nextFrame = prevFrame;
395
388
  // Execute this frame
396
389
  thisFrame.forEach(triggerCallback);
397
- /**
398
- * If we're recording stats then
399
- */
400
- if (stepName && statsBuffer.value) {
401
- statsBuffer.value.frameloop[stepName].push(numCalls);
402
- }
403
- numCalls = 0;
404
390
  // Clear the frame so no callbacks remain. This is to avoid
405
391
  // memory leaks should this render step not run for a while.
406
392
  thisFrame.clear();
@@ -425,7 +411,7 @@
425
411
  };
426
412
  const flagRunNextFrame = () => (runNextFrame = true);
427
413
  const steps = stepsOrder.reduce((acc, key) => {
428
- acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
414
+ acc[key] = createRenderStep(flagRunNextFrame);
429
415
  return acc;
430
416
  }, {});
431
417
  const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
@@ -481,7 +467,7 @@
481
467
  return { schedule, cancel, state, steps };
482
468
  }
483
469
 
484
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop$1, true);
470
+ const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
485
471
 
486
472
  let now;
487
473
  function clearTime() {
@@ -510,12 +496,6 @@
510
496
  },
511
497
  };
512
498
 
513
- const activeAnimations = {
514
- layout: 0,
515
- mainThread: 0,
516
- waapi: 0,
517
- };
518
-
519
499
  const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
520
500
  const isCSSVariableName =
521
501
  /*@__PURE__*/ checkStringStartsWith("--");
@@ -1503,7 +1483,7 @@
1503
1483
  for (let i = 0; i < numMixers; i++) {
1504
1484
  let mixer = mixerFactory(output[i], output[i + 1]);
1505
1485
  if (ease) {
1506
- const easingFunction = Array.isArray(ease) ? ease[i] || noop$1 : ease;
1486
+ const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
1507
1487
  mixer = pipe(easingFunction, mixer);
1508
1488
  }
1509
1489
  mixers.push(mixer);
@@ -1717,7 +1697,6 @@
1717
1697
  this.teardown();
1718
1698
  this.options.onStop?.();
1719
1699
  };
1720
- activeAnimations.mainThread++;
1721
1700
  this.options = options;
1722
1701
  this.initAnimation();
1723
1702
  this.play();
@@ -2022,7 +2001,6 @@
2022
2001
  this.state = "idle";
2023
2002
  this.stopDriver();
2024
2003
  this.startTime = this.holdTime = null;
2025
- activeAnimations.mainThread--;
2026
2004
  }
2027
2005
  stopDriver() {
2028
2006
  if (!this.driver)
@@ -2434,9 +2412,6 @@
2434
2412
  */
2435
2413
  if (Array.isArray(easing))
2436
2414
  keyframeOptions.easing = easing;
2437
- if (statsBuffer.value) {
2438
- activeAnimations.waapi++;
2439
- }
2440
2415
  const options = {
2441
2416
  delay,
2442
2417
  duration,
@@ -2447,13 +2422,7 @@
2447
2422
  };
2448
2423
  if (pseudoElement)
2449
2424
  options.pseudoElement = pseudoElement;
2450
- const animation = element.animate(keyframeOptions, options);
2451
- if (statsBuffer.value) {
2452
- animation.finished.finally(() => {
2453
- activeAnimations.waapi--;
2454
- });
2455
- }
2456
- return animation;
2425
+ return element.animate(keyframeOptions, options);
2457
2426
  }
2458
2427
 
2459
2428
  function isGenerator(type) {
@@ -2632,7 +2601,7 @@
2632
2601
  this.animation.rangeStart = rangeStart;
2633
2602
  if (rangeEnd)
2634
2603
  this.animation.rangeEnd = rangeEnd;
2635
- return noop$1;
2604
+ return noop;
2636
2605
  }
2637
2606
  else {
2638
2607
  return observe(this);
@@ -2985,7 +2954,7 @@
2985
2954
  }
2986
2955
  animation.finished.then(() => {
2987
2956
  this.notifyFinished();
2988
- }).catch(noop$1);
2957
+ }).catch(noop);
2989
2958
  if (this.pendingTimeline) {
2990
2959
  this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
2991
2960
  this.pendingTimeline = undefined;
@@ -5163,9 +5132,16 @@
5163
5132
  claimedPointerDownEvents.add(startEvent);
5164
5133
  }
5165
5134
  const onPressEnd = onPressStart(target, startEvent);
5135
+ /**
5136
+ * End listeners run in the capture phase so a descendant calling
5137
+ * stopPropagation() in its own pointerup handler can't prevent the
5138
+ * press gesture from ending. This also keeps the gesture-end
5139
+ * ordering consistent with the drag gesture. See #2794.
5140
+ */
5141
+ const endEventOptions = { ...eventOptions, capture: true };
5166
5142
  const onPointerEnd = (endEvent, success) => {
5167
- window.removeEventListener("pointerup", onPointerUp);
5168
- window.removeEventListener("pointercancel", onPointerCancel);
5143
+ window.removeEventListener("pointerup", onPointerUp, endEventOptions);
5144
+ window.removeEventListener("pointercancel", onPointerCancel, endEventOptions);
5169
5145
  if (isPressing.has(target)) {
5170
5146
  isPressing.delete(target);
5171
5147
  }
@@ -5185,8 +5161,8 @@
5185
5161
  const onPointerCancel = (cancelEvent) => {
5186
5162
  onPointerEnd(cancelEvent, false);
5187
5163
  };
5188
- window.addEventListener("pointerup", onPointerUp, eventOptions);
5189
- window.addEventListener("pointercancel", onPointerCancel, eventOptions);
5164
+ window.addEventListener("pointerup", onPointerUp, endEventOptions);
5165
+ window.addEventListener("pointercancel", onPointerCancel, endEventOptions);
5190
5166
  };
5191
5167
  targets.forEach((target) => {
5192
5168
  const pointerDownTarget = options.useGlobalTarget ? window : target;
@@ -5325,116 +5301,34 @@
5325
5301
  return () => cancelFrame(onFrame);
5326
5302
  }
5327
5303
 
5328
- function record() {
5329
- const { value } = statsBuffer;
5330
- if (value === null) {
5331
- cancelFrame(record);
5332
- return;
5333
- }
5334
- value.frameloop.rate.push(frameData.delta);
5335
- value.animations.mainThread.push(activeAnimations.mainThread);
5336
- value.animations.waapi.push(activeAnimations.waapi);
5337
- value.animations.layout.push(activeAnimations.layout);
5338
- }
5339
- function mean(values) {
5340
- return values.reduce((acc, value) => acc + value, 0) / values.length;
5341
- }
5342
- function summarise(values, calcAverage = mean) {
5343
- if (values.length === 0) {
5344
- return {
5345
- min: 0,
5346
- max: 0,
5347
- avg: 0,
5348
- };
5349
- }
5350
- return {
5351
- min: Math.min(...values),
5352
- max: Math.max(...values),
5353
- avg: calcAverage(values),
5354
- };
5355
- }
5356
- const msToFps = (ms) => Math.round(1000 / ms);
5304
+ const statsBuffer = {
5305
+ value: null,
5306
+ addProjectionMetrics: null,
5307
+ };
5308
+
5357
5309
  function clearStatsBuffer() {
5358
5310
  statsBuffer.value = null;
5359
5311
  statsBuffer.addProjectionMetrics = null;
5360
5312
  }
5361
- function reportStats() {
5362
- const { value } = statsBuffer;
5363
- if (!value) {
5364
- throw new Error("Stats are not being measured");
5365
- }
5366
- clearStatsBuffer();
5367
- cancelFrame(record);
5368
- const summary = {
5369
- frameloop: {
5370
- setup: summarise(value.frameloop.setup),
5371
- rate: summarise(value.frameloop.rate),
5372
- read: summarise(value.frameloop.read),
5373
- resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
5374
- preUpdate: summarise(value.frameloop.preUpdate),
5375
- update: summarise(value.frameloop.update),
5376
- preRender: summarise(value.frameloop.preRender),
5377
- render: summarise(value.frameloop.render),
5378
- postRender: summarise(value.frameloop.postRender),
5379
- },
5380
- animations: {
5381
- mainThread: summarise(value.animations.mainThread),
5382
- waapi: summarise(value.animations.waapi),
5383
- layout: summarise(value.animations.layout),
5384
- },
5385
- layoutProjection: {
5386
- nodes: summarise(value.layoutProjection.nodes),
5387
- calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
5388
- calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
5389
- },
5390
- };
5391
- /**
5392
- * Convert the rate to FPS
5393
- */
5394
- const { rate } = summary.frameloop;
5395
- rate.min = msToFps(rate.min);
5396
- rate.max = msToFps(rate.max);
5397
- rate.avg = msToFps(rate.avg);
5398
- [rate.min, rate.max] = [rate.max, rate.min];
5399
- return summary;
5400
- }
5401
5313
  function recordStats() {
5402
5314
  if (statsBuffer.value) {
5403
5315
  clearStatsBuffer();
5404
5316
  throw new Error("Stats are already being measured");
5405
5317
  }
5406
- const newStatsBuffer = statsBuffer;
5407
- newStatsBuffer.value = {
5408
- frameloop: {
5409
- setup: [],
5410
- rate: [],
5411
- read: [],
5412
- resolveKeyframes: [],
5413
- preUpdate: [],
5414
- update: [],
5415
- preRender: [],
5416
- render: [],
5417
- postRender: [],
5418
- },
5419
- animations: {
5420
- mainThread: [],
5421
- waapi: [],
5422
- layout: [],
5423
- },
5318
+ const buffer = statsBuffer;
5319
+ buffer.value = {
5424
5320
  layoutProjection: {
5425
5321
  nodes: [],
5426
5322
  calculatedTargetDeltas: [],
5427
5323
  calculatedProjections: [],
5428
5324
  },
5429
5325
  };
5430
- newStatsBuffer.addProjectionMetrics = (metrics) => {
5431
- const { layoutProjection } = newStatsBuffer.value;
5326
+ buffer.addProjectionMetrics = (metrics) => {
5327
+ const { layoutProjection } = buffer.value;
5432
5328
  layoutProjection.nodes.push(metrics.nodes);
5433
5329
  layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
5434
5330
  layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
5435
5331
  };
5436
- frame.postRender(record, true);
5437
- return reportStats;
5438
5332
  }
5439
5333
 
5440
5334
  /**
@@ -5720,14 +5614,154 @@
5720
5614
  */
5721
5615
  const findValueType = (v) => valueTypes.find(testValueType(v));
5722
5616
 
5617
+ let nameCount = 0;
5618
+ /**
5619
+ * Generated names live in their own namespace so we can tell a name we own
5620
+ * (and must clean up) from an author-defined one - and so a stale generated
5621
+ * name left behind by an interrupted transition is re-owned, not mistaken for
5622
+ * the author's and leaked.
5623
+ */
5624
+ const generatedName = () => `motion-view-${nameCount++}`;
5625
+ const isGeneratedName = (name) => name.startsWith("motion-view-");
5626
+ /**
5627
+ * Tag a captured element with a `view-transition-class` so authors can target
5628
+ * its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
5629
+ * the opaque generated name. Tracked in `classed` - separate from the generated
5630
+ * names in `assigned` - so cleanup removes the class without ever stripping an
5631
+ * author's own inline `view-transition-name`.
5632
+ */
5633
+ function tagClass(element, className, classed) {
5634
+ if (!className)
5635
+ return;
5636
+ element.style?.setProperty("view-transition-class", className);
5637
+ classed.push(element);
5638
+ }
5639
+ /**
5640
+ * Set the element's `view-transition-group` so its layer reconstructs the DOM
5641
+ * hierarchy in the pseudo-tree (`contain`) - or stays flat (`none`). Tracked in
5642
+ * `grouped` for cleanup. When the element clips (any non-`visible` overflow) its
5643
+ * name is recorded in `clipChildren` so the caller can clip the nested children
5644
+ * (`::view-transition-group-children(name)`), mirroring the live clip through
5645
+ * the whole transition rather than only at the live-DOM handoff.
5646
+ *
5647
+ * Ignored by browsers without nested view-transition groups, where it harmlessly
5648
+ * degrades to the flat default.
5649
+ */
5650
+ function applyGroup(element, name, group, grouped, clipChildren) {
5651
+ if (!group)
5652
+ return;
5653
+ element.style?.setProperty("view-transition-group", group);
5654
+ grouped.push(element);
5655
+ if (group !== "none" && clipChildren) {
5656
+ const style = getComputedStyle(element);
5657
+ if (style.overflowX !== "visible" || style.overflowY !== "visible") {
5658
+ clipChildren.add(name);
5659
+ }
5660
+ }
5661
+ }
5662
+ /**
5663
+ * Resolve a selector/Element to elements and ensure each one carries a
5664
+ * `view-transition-name` we can target from script.
5665
+ *
5666
+ * Author-defined names are reused as-is. Elements that are unnamed (or use
5667
+ * the browser's `auto`/`match-element`, whose generated name is not exposed
5668
+ * to script) are given a unique generated name, set inline so it's captured,
5669
+ * and tracked in `assigned` for later cleanup.
5670
+ *
5671
+ * `registry` maps each Element to its name so the same element keeps its name
5672
+ * across both captures (before and after the update), which is what allows a
5673
+ * persistent element to animate as a single `group` layer.
5674
+ */
5675
+ function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = [], group, grouped = [], clipChildren) {
5676
+ const elements = resolveElements(definition);
5677
+ /**
5678
+ * The new end of a paired morph: give each element the matching name from
5679
+ * the old end (by index) so the two share one layer and morph. If the new
5680
+ * end resolves to *more* elements than the old end named, the extras have no
5681
+ * counterpart - give them a fresh name so they animate as newcomers rather
5682
+ * than being silently left unnamed. We return the names actually assigned
5683
+ * (sized to the resolved elements), not the raw `forcedNames`, so stagger
5684
+ * totals and the layer set stay in step with what's on the page.
5685
+ */
5686
+ if (forcedNames) {
5687
+ return elements.map((element, i) => {
5688
+ const existing = registry.get(element);
5689
+ if (existing)
5690
+ return existing;
5691
+ const name = forcedNames[i] ?? generatedName();
5692
+ element.style?.setProperty("view-transition-name", name);
5693
+ assigned.push(element);
5694
+ registry.set(element, name);
5695
+ tagClass(element, className, classed);
5696
+ applyGroup(element, name, group, grouped, clipChildren);
5697
+ return name;
5698
+ });
5699
+ }
5700
+ /**
5701
+ * Read every current name up front, before assigning any. Interleaving the
5702
+ * reads with the inline `setProperty` writes below would dirty styles
5703
+ * between reads and force a style recalc per element; batching the reads
5704
+ * keeps it to one. Elements already in the registry keep their name and
5705
+ * need no read.
5706
+ */
5707
+ const currentNames = elements.map((element) => registry.has(element)
5708
+ ? undefined
5709
+ : getComputedStyle(element).getPropertyValue("view-transition-name"));
5710
+ return elements.map((element, i) => {
5711
+ const existing = registry.get(element);
5712
+ if (existing)
5713
+ return existing;
5714
+ const current = currentNames[i];
5715
+ let name;
5716
+ if (current &&
5717
+ current !== "none" &&
5718
+ current !== "auto" &&
5719
+ current !== "match-element" &&
5720
+ !isGeneratedName(current)) {
5721
+ /**
5722
+ * The author already named this layer - target it as-is and leave
5723
+ * it to them to clean up. `auto`/`match-element` are overridden
5724
+ * because their generated name is not exposed to script, and a
5725
+ * stale `motion-view-*` (e.g. left by an interrupted transition) is
5726
+ * re-owned rather than adopted as an author name and leaked.
5727
+ */
5728
+ name = current;
5729
+ }
5730
+ else {
5731
+ name = generatedName();
5732
+ element.style?.setProperty("view-transition-name", name);
5733
+ assigned.push(element);
5734
+ }
5735
+ registry.set(element, name);
5736
+ tagClass(element, className, classed);
5737
+ applyGroup(element, name, group, grouped, clipChildren);
5738
+ return name;
5739
+ });
5740
+ }
5741
+ /**
5742
+ * Remove the `view-transition-name`s we generated and the
5743
+ * `view-transition-class`es we applied. Author-defined names are never touched
5744
+ * (they're not in `assigned`). Safe to call more than once (e.g. on both a
5745
+ * finished and an interrupted transition).
5746
+ */
5747
+ function releaseViewTransitionNames(assigned, classed = [], grouped = []) {
5748
+ for (const element of assigned) {
5749
+ element.style?.removeProperty("view-transition-name");
5750
+ }
5751
+ for (const element of classed) {
5752
+ element.style?.removeProperty("view-transition-class");
5753
+ }
5754
+ for (const element of grouped) {
5755
+ element.style?.removeProperty("view-transition-group");
5756
+ }
5757
+ }
5758
+
5723
5759
  function chooseLayerType(valueName) {
5724
5760
  if (valueName === "layout")
5725
5761
  return "group";
5726
5762
  if (valueName === "enter" || valueName === "new")
5727
5763
  return "new";
5728
- if (valueName === "exit" || valueName === "old")
5729
- return "old";
5730
- return "group";
5764
+ return "old";
5731
5765
  }
5732
5766
 
5733
5767
  let pendingRules = {};
@@ -5762,21 +5796,21 @@
5762
5796
  };
5763
5797
 
5764
5798
  function getViewAnimationLayerInfo(pseudoElement) {
5765
- const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
5799
+ const match = pseudoElement.match(
5800
+ // `group-children` (nested transitions) before `group` so it wins.
5801
+ /::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
5766
5802
  if (!match)
5767
5803
  return null;
5768
5804
  return { layer: match[2], type: match[1] };
5769
5805
  }
5770
5806
 
5771
- function filterViewAnimations(animation) {
5772
- const { effect } = animation;
5773
- if (!effect)
5774
- return false;
5775
- return (effect.target === document.documentElement &&
5776
- effect.pseudoElement?.startsWith("::view-transition"));
5777
- }
5778
5807
  function getViewAnimations() {
5779
- return document.getAnimations().filter(filterViewAnimations);
5808
+ return document.getAnimations().filter((animation) => {
5809
+ const { effect } = animation;
5810
+ return (!!effect &&
5811
+ effect.target === document.documentElement &&
5812
+ effect.pseudoElement?.startsWith("::view-transition"));
5813
+ });
5780
5814
  }
5781
5815
 
5782
5816
  function hasTarget(target, targets) {
@@ -5784,87 +5818,432 @@
5784
5818
  }
5785
5819
 
5786
5820
  const definitionNames = ["layout", "enter", "exit", "new", "old"];
5821
+ /**
5822
+ * The `ViewTransitionTarget` buckets driving each generated layer type, in
5823
+ * priority order - the inverse of `chooseLayerType`. The new view is driven by
5824
+ * `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
5825
+ * bucket; they follow the default layout timing.
5826
+ */
5827
+ const typeBuckets = {
5828
+ group: ["layout"],
5829
+ new: ["new", "enter"],
5830
+ old: ["old", "exit"],
5831
+ };
5832
+ /**
5833
+ * Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
5834
+ * `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
5835
+ * from 1. `enter` prefers the matching `exit` value over these (see below).
5836
+ */
5837
+ const ORIGIN_DEFAULTS = {
5838
+ new: { opacity: 0, scale: 0.85 },
5839
+ old: { opacity: 1, scale: 1 },
5840
+ };
5841
+ /**
5842
+ * How much two box aspect ratios must differ before a morph is treated as
5843
+ * aspect-changing (and so worth cropping). Matches the projection engine's
5844
+ * `preserve-aspect` threshold, so small layout jitter doesn't trigger a crop.
5845
+ */
5846
+ const ASPECT_TOLERANCE = 0.2;
5787
5847
  function startViewAnimation(builder) {
5788
- const { update, targets, options: defaultOptions } = builder;
5848
+ const { update, targets, resolveDefs, cropOverride, pairs, classNames, flatGroups, options: defaultOptions, } = builder;
5789
5849
  if (!document.startViewTransition) {
5790
- return new Promise(async (resolve) => {
5850
+ // An async IIFE (not `new Promise(async )`) so a throwing/rejecting
5851
+ // update rejects this promise rather than leaving it unsettled.
5852
+ return (async () => {
5791
5853
  await update();
5792
- resolve(new GroupAnimation([]));
5793
- });
5854
+ return new GroupAnimation([]);
5855
+ })();
5794
5856
  }
5795
- // TODO: Go over existing targets and ensure they all have ids
5796
5857
  /**
5797
- * If we don't have any animations defined for the root target,
5798
- * remove it from being captured.
5858
+ * Resolve any selector/Element targets to layer names, assigning a
5859
+ * `view-transition-name` to each element as we go. We run this before the
5860
+ * update (so the elements are captured in the old snapshot) and again
5861
+ * after it (for the new snapshot). An element present in both keeps the
5862
+ * same name and animates as a single `group` layer.
5863
+ */
5864
+ const nameRegistry = new Map();
5865
+ const assigned = [];
5866
+ /**
5867
+ * Elements we tagged with a `view-transition-class` (via `.class()`),
5868
+ * tracked separately from `assigned` so cleanup removes the class without
5869
+ * ever stripping an author's own inline `view-transition-name`.
5870
+ */
5871
+ const classed = [];
5872
+ /**
5873
+ * Elements we set a `view-transition-group` on (for nesting), tracked for
5874
+ * cleanup. `clipChildren` collects the names of nested parents that clip in
5875
+ * the live layout, so their `::view-transition-group-children` is clipped
5876
+ * through the transition - not just once the live DOM takes back over.
5799
5877
  */
5800
- if (!hasTarget("root", targets)) {
5801
- css.set(":root", {
5802
- "view-transition-name": "none",
5878
+ const grouped = [];
5879
+ const clipChildren = new Set();
5880
+ const layerTargets = new Map();
5881
+ const croppedNames = new Set();
5882
+ /**
5883
+ * Each layer's explicit `.crop(true | false)` override (by resolved name),
5884
+ * so `finalizeCrop` can let an author's choice win over the morph default.
5885
+ */
5886
+ const cropForName = new Map();
5887
+ /**
5888
+ * Each layer's stagger position (index + total) within its subject, per
5889
+ * snapshot. Resolving against the snapshot the layer belongs to keeps
5890
+ * stagger correct when `update()` replaces the matched elements, and lets
5891
+ * us skip a layer that's absent from a snapshot (e.g. an exited element
5892
+ * has no `new` pseudo-element).
5893
+ */
5894
+ const layerStagger = new Map();
5895
+ /**
5896
+ * Names allocated for a paired subject in the old snapshot, replayed onto
5897
+ * its new-snapshot target so both ends share a layer and morph.
5898
+ */
5899
+ const pairNames = new Map();
5900
+ /**
5901
+ * The old (`from`) elements of each paired subject, so their names can be
5902
+ * transferred off before the new (`to`) elements inherit them.
5903
+ */
5904
+ const pairFrom = new Map();
5905
+ const resolveLayers = (phase) => {
5906
+ targets.forEach((target, definition) => {
5907
+ const className = classNames.get(definition);
5908
+ /**
5909
+ * Nest each resolved layer under its DOM-ancestor layer by default
5910
+ * (`contain`), so an ancestor's clip/transform/opacity reach it
5911
+ * through the transition; `.group(false)` opts a subject out (`none`)
5912
+ * to stay flat and escape. Skipped for root / pre-named layers, which
5913
+ * aren't elements we resolve and style.
5914
+ */
5915
+ const group = definition === "root" || !resolveDefs.has(definition)
5916
+ ? undefined
5917
+ : flatGroups.has(definition)
5918
+ ? "none"
5919
+ : "contain";
5920
+ let names;
5921
+ if (definition === "root" || !resolveDefs.has(definition)) {
5922
+ names = [definition];
5923
+ }
5924
+ else if (pairs.has(definition)) {
5925
+ /**
5926
+ * Paired morph: name the old target in the old snapshot, then
5927
+ * force the same name(s) onto the new target in the new one, so
5928
+ * two different elements morph as a single layer.
5929
+ */
5930
+ if (phase === "old") {
5931
+ pairFrom.set(definition, resolveElements(definition));
5932
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
5933
+ pairNames.set(definition, names);
5934
+ }
5935
+ else {
5936
+ /**
5937
+ * Transfer the name(s) off the `from` elements before the
5938
+ * `to` elements inherit them. A `from` that survives into
5939
+ * the new snapshot (e.g. hidden with `visibility: hidden`
5940
+ * rather than removed) would otherwise keep the name and
5941
+ * collide - "duplicate view-transition-name".
5942
+ */
5943
+ for (const el of pairFrom.get(definition) ?? []) {
5944
+ el.style?.removeProperty("view-transition-name");
5945
+ /**
5946
+ * Drop the old end from the registry too, so the new
5947
+ * end alone supplies this name's `new` crop radii - we
5948
+ * neither re-measure nor get ordered by a stale element.
5949
+ */
5950
+ nameRegistry.delete(el);
5951
+ }
5952
+ names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed, group, grouped, clipChildren);
5953
+ }
5954
+ }
5955
+ else {
5956
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
5957
+ }
5958
+ // Record any explicit `.crop(true | false)` per resolved name; the
5959
+ // crop set itself is computed later by `finalizeCrop` (it needs both
5960
+ // snapshots to know which morphs change aspect ratio).
5961
+ const override = cropOverride.get(definition);
5962
+ names.forEach((name, index) => {
5963
+ /**
5964
+ * If two subjects resolve to the same element, merge their
5965
+ * definitions so neither subject's animations are dropped.
5966
+ */
5967
+ const existing = layerTargets.get(name);
5968
+ layerTargets.set(name, existing && existing !== target
5969
+ ? { ...existing, ...target }
5970
+ : target);
5971
+ if (override !== undefined)
5972
+ cropForName.set(name, override);
5973
+ const stagger = layerStagger.get(name) ?? {};
5974
+ stagger[phase] = [index, names.length];
5975
+ layerStagger.set(name, stagger);
5976
+ });
5803
5977
  });
5804
- }
5978
+ };
5805
5979
  /**
5806
- * Set the timing curve to linear for all view transition layers.
5807
- * This gets baked into the keyframes, which can't be changed
5808
- * without breaking the generated animation.
5809
- *
5810
- * This allows us to set easing via updateTiming - which can be changed.
5980
+ * The stagger index/total for a layer, resolved against the snapshot it
5981
+ * belongs to. Returns index -1 when the layer is absent from that snapshot
5982
+ * so the caller can skip a pseudo-element that doesn't exist.
5811
5983
  */
5812
- css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
5813
- css.commit(); // Write
5814
- const transition = document.startViewTransition(async () => {
5815
- await update();
5816
- // TODO: Go over new targets and ensure they all have ids
5984
+ const staggerPosition = (name, type) => {
5985
+ const stagger = layerStagger.get(name);
5986
+ const position = type === "old"
5987
+ ? stagger?.old
5988
+ : type === "new"
5989
+ ? stagger?.new
5990
+ : // group / group-children / image-pair persist across both.
5991
+ stagger?.new ?? stagger?.old;
5992
+ return position ?? [-1, 1];
5993
+ };
5994
+ /**
5995
+ * Merge default + per-layer transition options for a generated layer and
5996
+ * resolve any stagger/delay function against this element's position. Used
5997
+ * by both the morph-retiming and crop corner-radius passes.
5998
+ */
5999
+ const resolveLayerTransition = (target, type, transitionName, index, total) => {
6000
+ const transition = mergeTransition(getValueTransition$1(defaultOptions, transitionName), getValueTransition$1((layerOptions(target, type) ?? {}), transitionName));
6001
+ if (typeof transition.delay === "function") {
6002
+ transition.delay = transition.delay(index, total);
6003
+ }
6004
+ return transition;
6005
+ };
6006
+ /**
6007
+ * Each layer's box size per snapshot, so `finalizeCrop` can tell whether a
6008
+ * morph's aspect ratio changed (the only case worth cropping).
6009
+ */
6010
+ const cropBox = new Map();
6011
+ const measureLayers = (phase) => nameRegistry.forEach((name, element) => {
6012
+ const rect = element.getBoundingClientRect?.();
6013
+ if (rect && rect.height) {
6014
+ const entry = cropBox.get(name) ?? {};
6015
+ entry[phase] = { width: rect.width, height: rect.height };
6016
+ cropBox.set(name, entry);
6017
+ }
5817
6018
  });
5818
- transition.finished.finally(() => {
6019
+ /**
6020
+ * With both snapshots measured, settle which layers crop. The default crops
6021
+ * only a morph whose aspect ratio *changes* between snapshots - the one case
6022
+ * where `object-fit: cover` does real work. A same-aspect morph or a
6023
+ * fade-only layer is left uncropped: its corners scale naturally (no flash
6024
+ * from squaring, no `overflow: clip` eating its shadow) and a backdrop can't
6025
+ * be clipped to nothing. An explicit `.crop(true | false)` overrides either
6026
+ * way. Runs after both snapshots are measured, since aspect needs both.
6027
+ */
6028
+ const finalizeCrop = () => {
6029
+ croppedNames.clear();
6030
+ for (const name of layerStagger.keys()) {
6031
+ if (name === "root")
6032
+ continue;
6033
+ // An explicit `.crop(true | false)` wins; otherwise crop a morph
6034
+ // whose aspect ratio changed.
6035
+ if (cropForName.get(name) ?? aspectChanged(name)) {
6036
+ croppedNames.add(name);
6037
+ }
6038
+ }
6039
+ };
6040
+ /**
6041
+ * Whether a layer is a morph whose box aspect ratio changed between
6042
+ * snapshots (beyond a small tolerance). Fade-only layers (one snapshot) are
6043
+ * never "changed".
6044
+ */
6045
+ const aspectChanged = (name) => {
6046
+ const box = cropBox.get(name);
6047
+ if (!box?.old || !box?.new || !box.old.height || !box.new.height) {
6048
+ return false;
6049
+ }
6050
+ return (Math.abs(box.old.width / box.old.height -
6051
+ box.new.width / box.new.height) > ASPECT_TOLERANCE);
6052
+ };
6053
+ /**
6054
+ * Write the persistent view-transition CSS: suppress root capture when the
6055
+ * root has no animations of its own; force linear timing (baked into the
6056
+ * keyframes, so we can retime later via updateTiming); and clip +
6057
+ * object-fit: cover every cropped morph (the UA default overflows on
6058
+ * aspect-ratio change).
6059
+ *
6060
+ * `css.commit` replaces rather than appends, so we re-set the full rule set
6061
+ * each call - the crop rules are only known after `finalizeCrop` runs in the
6062
+ * update callback, so the second call writes them.
6063
+ */
6064
+ const commitViewCSS = () => {
6065
+ if (!hasTarget("root", targets)) {
6066
+ css.set(":root", { "view-transition-name": "none" });
6067
+ }
6068
+ css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
6069
+ croppedNames.forEach((name) => {
6070
+ css.set(`::view-transition-group(${name})`, { overflow: "clip" });
6071
+ css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
6072
+ });
6073
+ /**
6074
+ * Clip the nested children of any layer that clips in the live layout,
6075
+ * so a wrapper crops its child for the whole morph (mirroring the DOM)
6076
+ * rather than only at the live-DOM handoff. No-op on browsers without
6077
+ * nested view-transition groups.
6078
+ */
6079
+ clipChildren.forEach((name) => {
6080
+ css.set(`::view-transition-group-children(${name})`, {
6081
+ overflow: "clip",
6082
+ });
6083
+ });
6084
+ css.commit(); // Write
6085
+ };
6086
+ const cleanup = () => {
6087
+ releaseViewTransitionNames(assigned, classed, grouped);
5819
6088
  css.remove(); // Write
5820
- });
5821
- return new Promise((resolve) => {
5822
- transition.ready.then(() => {
6089
+ };
6090
+ const callback = async () => {
6091
+ await update();
6092
+ /**
6093
+ * Re-resolve so elements created by the update are named for the new
6094
+ * snapshot, then measure them. With both snapshots measured we can
6095
+ * settle the crop set (aspect-changing morphs + forced).
6096
+ */
6097
+ resolveLayers("new");
6098
+ measureLayers("new");
6099
+ finalizeCrop();
6100
+ /**
6101
+ * Re-commit the crop CSS unconditionally: `finalizeCrop` is computed
6102
+ * here (after both snapshots are measured), so the clip rules must be
6103
+ * (re)written to match the settled set.
6104
+ */
6105
+ commitViewCSS();
6106
+ };
6107
+ let transition;
6108
+ try {
6109
+ resolveLayers("old");
6110
+ /**
6111
+ * Measure the old snapshot against the optimistic crop set (the new
6112
+ * snapshot doesn't exist yet, so aspect change can't be known here;
6113
+ * `finalizeCrop` settles it post-update).
6114
+ */
6115
+ measureLayers("old");
6116
+ commitViewCSS();
6117
+ transition = document.startViewTransition(callback);
6118
+ }
6119
+ catch (error) {
6120
+ /**
6121
+ * The prelude writes inline names before the transition exists. If it
6122
+ * throws (e.g. startViewTransition rejects in a bad UA state), release
6123
+ * them so we neither leak DOM state nor stall the queue on a promise
6124
+ * that never settles - hand back a rejection it can advance past.
6125
+ */
6126
+ cleanup();
6127
+ return Promise.reject(error);
6128
+ }
6129
+ transition.finished.finally(cleanup);
6130
+ return new Promise((resolve, reject) => {
6131
+ transition.ready
6132
+ .then(() => {
5823
6133
  const generatedViewAnimations = getViewAnimations();
5824
6134
  const animations = [];
5825
6135
  /**
5826
6136
  * Create animations for each of our explicitly-defined subjects.
6137
+ * `opacityAnimated` additionally tracks which `${name}:${type}`
6138
+ * we faded, so we can keep the UA `plus-lighter` blend only for a
6139
+ * genuine opacity crossfade (both sides fading) and drop it for a
6140
+ * slide/transform, where additive compositing would flash bright.
5827
6141
  */
5828
- targets.forEach((definition, target) => {
5829
- // TODO: If target is not "root", resolve elements
5830
- // and iterate over each
6142
+ const explicitlyAnimated = new Set();
6143
+ const opacityAnimated = new Set();
6144
+ layerTargets.forEach((target, name) => {
6145
+ const stagger = layerStagger.get(name);
6146
+ /**
6147
+ * Presence: `enter` only fires for a pure newcomer (a new
6148
+ * view with no old), `exit` only for a pure leaver. A
6149
+ * survivor (both) gets neither - it just morphs.
6150
+ */
6151
+ const enterApplies = !!stagger?.new && !stagger?.old;
6152
+ const exitApplies = !!stagger?.old && !stagger?.new;
5831
6153
  for (const key of definitionNames) {
5832
- if (!definition[key])
6154
+ if (!target[key])
6155
+ continue;
6156
+ if (key === "enter" && !enterApplies)
6157
+ continue;
6158
+ if (key === "exit" && !exitApplies)
6159
+ continue;
6160
+ const type = chooseLayerType(key);
6161
+ const [index, total] = staggerPosition(name, type);
6162
+ // Skip a layer absent from its snapshot.
6163
+ if (index === -1)
5833
6164
  continue;
5834
- const { keyframes, options } = definition[key];
6165
+ const { keyframes, options } = target[key];
5835
6166
  for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
5836
- if (!valueKeyframes)
6167
+ // Skip only missing values - `0` (e.g. opacity: 0)
6168
+ // is valid and must reach the from-value inference.
6169
+ if (valueKeyframes == null)
5837
6170
  continue;
5838
- const valueOptions = {
5839
- ...getValueTransition$1(defaultOptions, valueName),
5840
- ...getValueTransition$1(options, valueName),
5841
- };
5842
- const type = chooseLayerType(key);
5843
6171
  /**
5844
- * If this is an opacity animation, and keyframes are not an array,
5845
- * we need to convert them into an array and set an initial value.
6172
+ * The view path hands keyframes straight to WAAPI,
6173
+ * so Motion's `x`/`y` shorthands (compiled to
6174
+ * `transform` only via the value pipeline) have no
6175
+ * effect. Warn and skip - use `transform`/`translate`.
5846
6176
  */
5847
- if (valueName === "opacity" &&
5848
- !Array.isArray(valueKeyframes)) {
5849
- const initialValue = type === "new" ? 0 : 1;
5850
- valueKeyframes = [initialValue, valueKeyframes];
6177
+ if (valueName === "x" || valueName === "y") {
6178
+ warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
6179
+ continue;
5851
6180
  }
5852
6181
  /**
5853
- * Resolve stagger function if provided.
6182
+ * enter/exit win over new/old on a shared property -
6183
+ * skip it here when the gated bucket also defines it.
6184
+ */
6185
+ if (key === "new" &&
6186
+ enterApplies &&
6187
+ target.enter?.keyframes[valueName] != null) {
6188
+ continue;
6189
+ }
6190
+ if (key === "old" &&
6191
+ exitApplies &&
6192
+ target.exit?.keyframes[valueName] != null) {
6193
+ continue;
6194
+ }
6195
+ const valueOptions = mergeTransition(getValueTransition$1(defaultOptions, valueName), getValueTransition$1(options, valueName));
6196
+ /**
6197
+ * Infer an origin for a single-value keyframe. An
6198
+ * `enter` mirrors the matching `exit` value (a
6199
+ * defined exit reverses into the enter for free);
6200
+ * otherwise the per-type default (opacity 0/1, scale
6201
+ * 0.85). No default -> left as-is (animates from the
6202
+ * live value).
6203
+ *
6204
+ * `new`/`old` fire for survivors too, where only the
6205
+ * opacity crossfade default applies - a transform
6206
+ * default like scale 0.85 would pop a persisting
6207
+ * element, so gate it on the layer actually
6208
+ * entering/leaving.
6209
+ */
6210
+ if (!Array.isArray(valueKeyframes)) {
6211
+ const exitValue = key === "enter"
6212
+ ? target.exit?.keyframes[valueName]
6213
+ : undefined;
6214
+ const allowDefault = valueName === "opacity" ||
6215
+ (type === "new" ? enterApplies : exitApplies);
6216
+ const from = exitValue != null
6217
+ ? Array.isArray(exitValue)
6218
+ ? exitValue[exitValue.length - 1]
6219
+ : exitValue
6220
+ : allowDefault
6221
+ ? ORIGIN_DEFAULTS[type]?.[valueName]
6222
+ : undefined;
6223
+ if (from !== undefined) {
6224
+ valueKeyframes = [from, valueKeyframes];
6225
+ }
6226
+ }
6227
+ /**
6228
+ * Resolve stagger function if provided, per element
6229
+ * across this subject's resolved layers.
5854
6230
  */
5855
6231
  if (typeof valueOptions.delay === "function") {
5856
- valueOptions.delay = valueOptions.delay(0, 1);
6232
+ valueOptions.delay = valueOptions.delay(index, total);
5857
6233
  }
5858
6234
  valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
5859
6235
  valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
5860
- const animation = new NativeAnimation({
6236
+ animations.push(new NativeAnimation({
5861
6237
  ...valueOptions,
5862
6238
  element: document.documentElement,
5863
6239
  name: valueName,
5864
- pseudoElement: `::view-transition-${type}(${target})`,
6240
+ pseudoElement: `::view-transition-${type}(${name})`,
5865
6241
  keyframes: valueKeyframes,
5866
- });
5867
- animations.push(animation);
6242
+ }));
6243
+ explicitlyAnimated.add(`${name}:${type}`);
6244
+ if (valueName === "opacity") {
6245
+ opacityAnimated.add(`${name}:${type}`);
6246
+ }
5868
6247
  }
5869
6248
  }
5870
6249
  });
@@ -5883,45 +6262,129 @@
5883
6262
  const name = getViewAnimationLayerInfo(pseudoElement);
5884
6263
  if (!name)
5885
6264
  continue;
5886
- const targetDefinition = targets.get(name.layer);
5887
- if (!targetDefinition) {
5888
- /**
5889
- * If transition name is group then update the timing of the animation
5890
- * whereas if it's old or new then we could possibly replace it using
5891
- * the above method.
5892
- */
5893
- const transitionName = name.type === "group" ? "layout" : "";
5894
- let animationTransition = {
5895
- ...getValueTransition$1(defaultOptions, transitionName),
5896
- };
5897
- animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
5898
- animationTransition =
5899
- applyGeneratorOptions(animationTransition);
5900
- const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
5901
- effect.updateTiming({
5902
- delay: secondsToMilliseconds(animationTransition.delay ?? 0),
5903
- duration: animationTransition.duration,
5904
- easing,
5905
- });
5906
- animations.push(new NativeAnimationWrapper(animation));
5907
- }
5908
- else if (hasOpacity(targetDefinition, "enter") &&
5909
- hasOpacity(targetDefinition, "exit") &&
5910
- effect
5911
- .getKeyframes()
5912
- .some((keyframe) => keyframe.mixBlendMode)) {
5913
- animations.push(new NativeAnimationWrapper(animation));
5914
- }
5915
- else {
5916
- animation.cancel();
6265
+ const targetDefinition = layerTargets.get(name.layer);
6266
+ /**
6267
+ * We built our own animation for this layer, so drop the
6268
+ * browser-generated fade we're replacing. The UA
6269
+ * `plus-lighter` blend is a *separate* generated animation on
6270
+ * the same pseudo (it sets `mix-blend-mode` in its keyframes):
6271
+ * keep it *only* for a true opacity crossfade - both sides
6272
+ * fading - so a symmetric crossfade composites without
6273
+ * darkening, but a slide/transform (where both layers stay
6274
+ * opaque and overlap) doesn't flash bright from the addition.
6275
+ */
6276
+ if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
6277
+ const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
6278
+ opacityAnimated.has(`${name.layer}:old`);
6279
+ if (isCrossfade &&
6280
+ effect
6281
+ .getKeyframes()
6282
+ .some((keyframe) => keyframe.mixBlendMode)) {
6283
+ animations.push(new NativeAnimationWrapper(animation));
6284
+ }
6285
+ else {
6286
+ animation.cancel();
6287
+ }
6288
+ continue;
5917
6289
  }
6290
+ /**
6291
+ * Otherwise retime the browser-generated animation to
6292
+ * Motion's timing. This auto-enables the layout (group)
6293
+ * morph for any resolved/named target, and applies the
6294
+ * default timing to old/new layers we haven't explicitly
6295
+ * overridden.
6296
+ *
6297
+ * group + group-children both follow the layout timing so
6298
+ * the nesting container stays in sync with the morph.
6299
+ */
6300
+ /**
6301
+ * A survivor's old + new are the two halves of one
6302
+ * `plus-lighter` crossfade. They must share identical timing
6303
+ * (so their opacities stay mirrored and sum to 1 - else the
6304
+ * additive blend flashes bright wherever both are partly
6305
+ * visible) and fade linearly (the bounce belongs on the
6306
+ * group's geometry, not the opacity). So time them as the
6307
+ * group, rather than via their own - potentially staggered,
6308
+ * or enter/exit-derived - old/new options.
6309
+ */
6310
+ const stagger = layerStagger.get(name.layer);
6311
+ const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
6312
+ !!stagger?.old &&
6313
+ !!stagger?.new;
6314
+ const timingType = name.type.startsWith("group") || isMorphCrossfade
6315
+ ? "group"
6316
+ : name.type;
6317
+ const [index, total] = staggerPosition(name.layer, timingType);
6318
+ const transitionName = timingType === "group" ? "layout" : "";
6319
+ let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
6320
+ /**
6321
+ * The crossfade should resolve at the spring's *perceptual*
6322
+ * (visual) duration - the geometry can keep bouncing, but the
6323
+ * opacity shouldn't drag through the settle. So capture
6324
+ * `visualDuration` before `applyGeneratorOptions` replaces it
6325
+ * with the full overshoot duration, and use it for the fade.
6326
+ */
6327
+ const visualDuration = animationTransition.visualDuration;
6328
+ animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
6329
+ animationTransition =
6330
+ applyGeneratorOptions(animationTransition);
6331
+ const duration = isMorphCrossfade && visualDuration !== undefined
6332
+ ? secondsToMilliseconds(visualDuration)
6333
+ : animationTransition.duration;
6334
+ const easing = isMorphCrossfade
6335
+ ? "linear"
6336
+ : mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
6337
+ effect.updateTiming({
6338
+ delay: secondsToMilliseconds(animationTransition.delay ?? 0),
6339
+ duration,
6340
+ easing,
6341
+ });
6342
+ animations.push(new NativeAnimationWrapper(animation));
5918
6343
  }
5919
6344
  resolve(new GroupAnimation(animations));
5920
- });
6345
+ })
6346
+ .catch(() =>
6347
+ /**
6348
+ * `ready` rejects when the transition is skipped - no visual
6349
+ * change, or superseded by an interrupting transition. The DOM
6350
+ * update still applied, so settle with no animations rather than
6351
+ * surfacing it as an error to an awaiting caller. A genuine
6352
+ * failure in `update()` rejects `updateCallbackDone` (already
6353
+ * settled by now), so propagate that instead.
6354
+ */
6355
+ transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
5921
6356
  });
5922
6357
  }
5923
- function hasOpacity(target, key) {
5924
- return target?.[key]?.keyframes.opacity;
6358
+ /**
6359
+ * The options that should time a given generated layer type, so a retimed
6360
+ * group/old/new picks up any per-target transition the user provided. Checks
6361
+ * the type's buckets in priority order (e.g. `new` before `enter`).
6362
+ */
6363
+ function layerOptions(target, type) {
6364
+ for (const bucket of typeBuckets[type] ?? []) {
6365
+ const options = target?.[bucket]?.options;
6366
+ if (options)
6367
+ return options;
6368
+ }
6369
+ }
6370
+ /**
6371
+ * Merge a base transition (e.g. the default `options`) with a per-layer/value
6372
+ * override. An explicit `duration` on the override must win over an inherited
6373
+ * generator's own timing: a spring prefers `visualDuration`, and
6374
+ * `spring.applyToOptions` overwrites `duration` with the computed settle time -
6375
+ * so without this the override is silently discarded. Dropping the inherited
6376
+ * `type`/`visualDuration` makes the layer a plain tween of that duration, unless
6377
+ * it asked for its own generator `type`/`visualDuration`.
6378
+ */
6379
+ function mergeTransition(base, override) {
6380
+ const merged = { ...base, ...override };
6381
+ if (override.duration !== undefined) {
6382
+ if (override.visualDuration === undefined)
6383
+ delete merged.visualDuration;
6384
+ if (override.type === undefined)
6385
+ delete merged.type;
6386
+ }
6387
+ return merged;
5925
6388
  }
5926
6389
 
5927
6390
  let builders = [];
@@ -5935,10 +6398,16 @@
5935
6398
  function start(builder) {
5936
6399
  removeItem(builders, builder);
5937
6400
  current = builder;
5938
- startViewAnimation(builder).then((animation) => {
6401
+ startViewAnimation(builder)
6402
+ .then((animation) => {
5939
6403
  builder.notifyReady(animation);
5940
- animation.finished.finally(next);
5941
- });
6404
+ return animation.finished;
6405
+ })
6406
+ // A genuinely failed transition (a throwing update) rejects the
6407
+ // builder; a skipped/interrupted one resolves with no animations (see
6408
+ // start.ts). Either way, advance the queue - else later transitions hang.
6409
+ .catch((error) => builder.notifyReject(error))
6410
+ .finally(next);
5942
6411
  }
5943
6412
  function processQueue() {
5944
6413
  /**
@@ -5975,31 +6444,129 @@
5975
6444
  constructor(update, options = {}) {
5976
6445
  this.currentSubject = "root";
5977
6446
  this.targets = new Map();
5978
- this.notifyReady = noop$1;
5979
- this.readyPromise = new Promise((resolve) => {
6447
+ /**
6448
+ * Definitions that must be resolved to elements (and assigned a
6449
+ * `view-transition-name`) rather than treated as pre-named layers.
6450
+ */
6451
+ this.resolveDefs = new Set();
6452
+ /**
6453
+ * Per-subject crop override: `true` forces the crop (clip + object-fit:
6454
+ * cover + animated corner radii) on, `false` forces it off. A subject with
6455
+ * no entry uses the default - crop only a genuine morph (a layer present in
6456
+ * both snapshots), so a fade-only enter/exit isn't clipped to nothing.
6457
+ */
6458
+ this.cropOverride = new Map();
6459
+ /**
6460
+ * Subjects paired with a different new-snapshot target (the second `.add()`
6461
+ * argument), so two distinct elements share one name and morph into each
6462
+ * other - a shared-element transition.
6463
+ */
6464
+ this.pairs = new Map();
6465
+ /**
6466
+ * A `view-transition-class` to apply to each subject's resolved elements,
6467
+ * so authors can target the generated layers from CSS by class rather than
6468
+ * the opaque generated name.
6469
+ */
6470
+ this.classNames = new Map();
6471
+ /**
6472
+ * Subjects opted out of automatic group nesting via `.group(false)`. Their
6473
+ * layer stays a flat top-level group (`view-transition-group: none`) instead
6474
+ * of nesting under its DOM-ancestor layer - so it animates independently and
6475
+ * escapes an ancestor's clip/transform (e.g. an element that lifts out of a
6476
+ * card and flies across, which nesting would clip to the card).
6477
+ */
6478
+ this.flatGroups = new Set();
6479
+ this.notifyReady = noop;
6480
+ this.notifyReject = noop;
6481
+ this.readyPromise = new Promise((resolve, reject) => {
5980
6482
  this.notifyReady = resolve;
6483
+ this.notifyReject = reject;
5981
6484
  });
5982
6485
  this.update = update;
5983
6486
  this.options = {
5984
6487
  interrupt: "wait",
5985
6488
  ...options,
5986
6489
  };
6490
+ // Avoid an unhandled rejection when a failed transition has no
6491
+ // `.then(_, reject)` handler attached (e.g. fire-and-forget).
6492
+ this.readyPromise.catch(noop);
5987
6493
  addToQueue(this);
5988
6494
  }
5989
- get(subject) {
6495
+ /**
6496
+ * Target elements resolved from a selector or Element, each assigned a
6497
+ * `view-transition-name` automatically.
6498
+ *
6499
+ * Passing a second target pairs them: the first is resolved in the old
6500
+ * snapshot and the second in the new, sharing one name so two *different*
6501
+ * elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
6502
+ * pass them the other way round to morph back.
6503
+ */
6504
+ add(subject, newSubject) {
5990
6505
  this.currentSubject = subject;
6506
+ this.resolveDefs.add(subject);
6507
+ if (newSubject !== undefined)
6508
+ this.pairs.set(subject, newSubject);
6509
+ // Register the subject so it participates (and gets an automatic
6510
+ // layout/morph animation) even without an explicit enter/exit/layout.
6511
+ if (!this.targets.has(subject))
6512
+ this.targets.set(subject, {});
5991
6513
  return this;
5992
6514
  }
5993
- layout(keyframes, options) {
5994
- this.updateTarget("layout", keyframes, options);
6515
+ /**
6516
+ * Control this subject's crop (clip + `object-fit: cover` + animated
6517
+ * corners). By default a subject auto-crops only when it actually morphs -
6518
+ * present in both snapshots (a survivor, or an `.add(a, b)` pair). A
6519
+ * fade-only enter/exit has no second box to crop against, so it's left to
6520
+ * the browser default; in particular the `overflow: clip` a crop adds would
6521
+ * otherwise clip a mis-sized enter/exit layer to nothing.
6522
+ *
6523
+ * `.crop(false)` forces the crop off (e.g. a text morph, where
6524
+ * `object-fit: cover` clips glyphs as the box grows); `.crop(true)` forces
6525
+ * it on for a non-morph the default wouldn't otherwise crop.
6526
+ */
6527
+ crop(enabled = true) {
6528
+ this.cropOverride.set(this.currentSubject, enabled);
5995
6529
  return this;
5996
6530
  }
5997
- new(keyframes, options) {
5998
- this.updateTarget("new", keyframes, options);
6531
+ /**
6532
+ * By default a subject's layer nests under its nearest DOM-ancestor layer
6533
+ * (`view-transition-group: contain`), so the ancestor's clip/transform/opacity
6534
+ * apply to it through the transition - mirroring how the DOM actually paints,
6535
+ * and letting a wrapper crop its child for the whole morph rather than only
6536
+ * once the live DOM takes back over. (Needs a browser that supports nested
6537
+ * view-transition groups; elsewhere it degrades to the flat default.)
6538
+ *
6539
+ * Call `.group(false)` to opt out: the layer stays flat and top-level, so it
6540
+ * animates independently and escapes an ancestor's clip - e.g. an element
6541
+ * that should lift out of a card and fly across, which nesting would clip.
6542
+ */
6543
+ group(enabled = true) {
6544
+ enabled
6545
+ ? this.flatGroups.delete(this.currentSubject)
6546
+ : this.flatGroups.add(this.currentSubject);
5999
6547
  return this;
6000
6548
  }
6001
- old(keyframes, options) {
6002
- this.updateTarget("old", keyframes, options);
6549
+ /**
6550
+ * Tag this subject's generated layers with a `view-transition-class`, so
6551
+ * they can be targeted from CSS - `::view-transition-group(.name)`,
6552
+ * `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
6553
+ * - without the opaque generated `view-transition-name`. Because `.add()`
6554
+ * can match many elements, a shared class targets them all at once (and,
6555
+ * for a pair, both ends). The escape hatch for z-index / custom keyframes
6556
+ * on a morph layer.
6557
+ */
6558
+ class(name) {
6559
+ this.classNames.set(this.currentSubject, name);
6560
+ return this;
6561
+ }
6562
+ /**
6563
+ * Set the transition for this subject's morph. The morph is enabled
6564
+ * automatically by `.add()`; this just customises its timing (duration,
6565
+ * easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
6566
+ * opts the page into the transition (the root crossfade).
6567
+ */
6568
+ layout(options = {}) {
6569
+ this.updateTarget("layout", {}, options);
6003
6570
  return this;
6004
6571
  }
6005
6572
  enter(keyframes, options) {
@@ -6010,9 +6577,21 @@
6010
6577
  this.updateTarget("exit", keyframes, options);
6011
6578
  return this;
6012
6579
  }
6013
- crossfade(options) {
6014
- this.updateTarget("enter", { opacity: 1 }, options);
6015
- this.updateTarget("exit", { opacity: 0 }, options);
6580
+ /**
6581
+ * Animate the new view directly, whether the element is appearing or
6582
+ * persisting (unlike `.enter()`, which only fires for a pure newcomer).
6583
+ * Pair with `.old()` for a crossfade or slide-through.
6584
+ */
6585
+ new(keyframes, options) {
6586
+ this.updateTarget("new", keyframes, options);
6587
+ return this;
6588
+ }
6589
+ /**
6590
+ * Animate the old view directly, whether the element is leaving or
6591
+ * persisting (unlike `.exit()`, which only fires for a pure leaver).
6592
+ */
6593
+ old(keyframes, options) {
6594
+ this.updateTarget("old", keyframes, options);
6016
6595
  return this;
6017
6596
  }
6018
6597
  updateTarget(target, keyframes, options = {}) {
@@ -6027,8 +6606,8 @@
6027
6606
  return this.readyPromise.then(resolve, reject);
6028
6607
  }
6029
6608
  }
6030
- function animateView(update, defaultOptions = {}) {
6031
- return new ViewTransitionBuilder(update, defaultOptions);
6609
+ function animateView(update, options = {}) {
6610
+ return new ViewTransitionBuilder(update, options);
6032
6611
  }
6033
6612
 
6034
6613
  const createAxisDelta = () => ({
@@ -8128,7 +8707,7 @@
8128
8707
  : values.borderRadius;
8129
8708
  }
8130
8709
  const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
8131
- const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop$1);
8710
+ const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
8132
8711
  function compress(min, max, easing) {
8133
8712
  return (p) => {
8134
8713
  // Could replace ifs with clamp
@@ -8148,7 +8727,7 @@
8148
8727
 
8149
8728
  function addDomEvent(target, eventName, handler, options = { passive: true }) {
8150
8729
  target.addEventListener(eventName, handler, options);
8151
- return () => target.removeEventListener(eventName, handler);
8730
+ return () => target.removeEventListener(eventName, handler, options);
8152
8731
  }
8153
8732
 
8154
8733
  const compareByDepth = (a, b) => a.depth - b.depth;
@@ -9418,7 +9997,6 @@
9418
9997
  */
9419
9998
  this.pendingAnimation = frame.update(() => {
9420
9999
  globalProjectionState.hasAnimatedSinceResize = true;
9421
- activeAnimations.layout++;
9422
10000
  this.motionValue || (this.motionValue = motionValue(0));
9423
10001
  this.motionValue.jump(0, false);
9424
10002
  this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
@@ -9429,11 +10007,7 @@
9429
10007
  this.mixTargetDelta(latest);
9430
10008
  options.onUpdate && options.onUpdate(latest);
9431
10009
  },
9432
- onStop: () => {
9433
- activeAnimations.layout--;
9434
- },
9435
10010
  onComplete: () => {
9436
- activeAnimations.layout--;
9437
10011
  options.onComplete && options.onComplete();
9438
10012
  this.completeAnimation();
9439
10013
  },
@@ -9948,7 +10522,7 @@
9948
10522
  */
9949
10523
  const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
9950
10524
  ? Math.round
9951
- : noop$1;
10525
+ : noop;
9952
10526
  function roundAxis(axis) {
9953
10527
  // Round to the nearest .5 pixels to support subpixel layouts
9954
10528
  axis.min = roundPoint(axis.min);
@@ -10022,39 +10596,221 @@
10022
10596
  checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
10023
10597
  });
10024
10598
 
10025
- const layoutSelector = "[data-layout], [data-layout-id]";
10026
- const noop = () => { };
10027
- function snapshotFromTarget(projection) {
10028
- const target = projection.targetWithTransforms || projection.target;
10029
- if (!target)
10030
- return undefined;
10031
- const measuredBox = createBox();
10032
- const layoutBox = createBox();
10033
- copyBoxInto(measuredBox, target);
10034
- copyBoxInto(layoutBox, target);
10599
+ const layoutSelector = "[data-layout],[data-layout-id]";
10600
+ /**
10601
+ * All imperatively-created projection nodes live in one persistent tree,
10602
+ * shared across animateLayout() calls (and with any React-created nodes,
10603
+ * via the singleton document root). Keyed by element for reuse.
10604
+ */
10605
+ const layoutNodes = new WeakMap();
10606
+ /**
10607
+ * Builders created within the same synchronous tick are flushed together
10608
+ * as a single "commit": every node is snapshotted before any updateDom
10609
+ * runs, mirroring React batching renders from different parts of the tree.
10610
+ */
10611
+ let pendingBuilders;
10612
+ function collectLayoutElements(scope) {
10613
+ const elements = [];
10614
+ if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
10615
+ elements.push(scope);
10616
+ }
10617
+ scope.querySelectorAll(layoutSelector).forEach((element) => {
10618
+ if (element instanceof HTMLElement)
10619
+ elements.push(element);
10620
+ });
10621
+ return elements;
10622
+ }
10623
+ /**
10624
+ * Process any work scheduled on the frameloop now. A previous animation
10625
+ * may have been seeked while paused (controls.time = x) without a frame
10626
+ * having rendered it - we must materialise that state into the DOM
10627
+ * before taking snapshots.
10628
+ */
10629
+ function flushPendingFrame() {
10630
+ if (frameData.isProcessing)
10631
+ return;
10632
+ const now = time.now();
10633
+ frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
10634
+ frameData.timestamp = now;
10635
+ frameData.isProcessing = true;
10636
+ frameSteps.update.process(frameData);
10637
+ frameSteps.preRender.process(frameData);
10638
+ frameSteps.render.process(frameData);
10639
+ frameData.isProcessing = false;
10640
+ }
10641
+ function getProjectionParent(element) {
10642
+ let ancestor = element.parentElement;
10643
+ while (ancestor) {
10644
+ const node = layoutNodes.get(ancestor);
10645
+ if (node && node.instance)
10646
+ return node;
10647
+ ancestor = ancestor.parentElement;
10648
+ }
10649
+ return undefined;
10650
+ }
10651
+ function createVisualElement() {
10652
+ return new HTMLVisualElement({
10653
+ props: {},
10654
+ presenceContext: null,
10655
+ visualState: {
10656
+ latestValues: {},
10657
+ renderState: {
10658
+ transform: {},
10659
+ transformOrigin: {},
10660
+ style: {},
10661
+ vars: {},
10662
+ },
10663
+ },
10664
+ }, { allowProjection: true });
10665
+ }
10666
+ function readNodeOptions(element, transition) {
10667
+ const layoutAttr = element.getAttribute("data-layout");
10668
+ const layoutId = element.getAttribute("data-layout-id") ?? undefined;
10035
10669
  return {
10036
- animationId: projection.root?.animationId ?? 0,
10037
- measuredBox,
10038
- layoutBox,
10039
- latestValues: projection.animationValues || projection.latestValues || {},
10040
- source: projection.id,
10670
+ layoutId,
10671
+ layout: layoutAttr !== null ? true : undefined,
10672
+ animationType: (!layoutAttr || layoutAttr === "true"
10673
+ ? "both"
10674
+ : layoutAttr),
10675
+ transition,
10041
10676
  };
10042
10677
  }
10678
+ function prepareNode(element, transition) {
10679
+ let node = layoutNodes.get(element);
10680
+ if (!node) {
10681
+ let visualElement = visualElementStore.get(element);
10682
+ if (!visualElement)
10683
+ visualElement = createVisualElement();
10684
+ /**
10685
+ * A first-time element may carry a projection transform in its
10686
+ * inline style (e.g. it was cloned from an element mid-animation).
10687
+ * That transform isn't tracked in latestValues so the engine can't
10688
+ * reset it before measuring - clear it now so the first layout
10689
+ * measurement isn't inflated.
10690
+ */
10691
+ if (element.style.transform &&
10692
+ !hasTransform(visualElement.latestValues)) {
10693
+ element.style.transform = "";
10694
+ }
10695
+ node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
10696
+ visualElement.projection = node;
10697
+ node.setOptions({
10698
+ ...readNodeOptions(element, transition),
10699
+ visualElement,
10700
+ });
10701
+ node.mount(element);
10702
+ layoutNodes.set(element, node);
10703
+ }
10704
+ else {
10705
+ node.setOptions(readNodeOptions(element, transition));
10706
+ }
10707
+ node.isPresent = true;
10708
+ if (node.options.onExitComplete) {
10709
+ node.setOptions({ onExitComplete: undefined });
10710
+ }
10711
+ return node;
10712
+ }
10713
+ function sortDocumentOrder(elements) {
10714
+ return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
10715
+ }
10716
+ function dropNode(element, node) {
10717
+ node.setOptions({ onExitComplete: undefined });
10718
+ /**
10719
+ * Stop any lingering animation so it can't leak into future updates.
10720
+ * A follow node can share its currentAnimation with a surviving lead
10721
+ * (via resumingFrom), in which case it isn't ours to stop.
10722
+ */
10723
+ const stack = node.getStack();
10724
+ if (!stack || node.isLead())
10725
+ node.currentAnimation?.stop();
10726
+ node.unmount();
10727
+ layoutNodes.delete(element);
10728
+ }
10729
+ function flushPendingBuilders() {
10730
+ const builders = pendingBuilders;
10731
+ pendingBuilders = undefined;
10732
+ flushPendingFrame();
10733
+ /**
10734
+ * Discover and mount every node across all builders before snapshotting
10735
+ * any of them. Mounting during an active update flags isLayoutDirty,
10736
+ * which would make that node's own willUpdate skip its snapshot.
10737
+ * Document order guarantees ancestors mount before descendants, even
10738
+ * when they're discovered by different builders.
10739
+ */
10740
+ const targets = new Map();
10741
+ for (const builder of builders) {
10742
+ for (const element of builder.collectTargets()) {
10743
+ const owners = targets.get(element);
10744
+ owners ? owners.push(builder) : targets.set(element, [builder]);
10745
+ }
10746
+ }
10747
+ const union = new Map();
10748
+ for (const element of sortDocumentOrder(targets.keys())) {
10749
+ const owners = targets.get(element);
10750
+ const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
10751
+ for (const owner of owners)
10752
+ owner.adopt(element, node);
10753
+ union.set(element, node);
10754
+ }
10755
+ union.forEach((node) => {
10756
+ node.isLayoutDirty = false;
10757
+ node.willUpdate();
10758
+ });
10759
+ const updatePromises = [];
10760
+ for (const builder of builders) {
10761
+ const result = builder.runUpdate();
10762
+ if (result)
10763
+ updatePromises.push(result);
10764
+ }
10765
+ const commit = () => {
10766
+ /**
10767
+ * Process all additions before any removals so that, even across
10768
+ * builders, a removed member knows whether a replacement with the
10769
+ * same layoutId was added in this commit.
10770
+ */
10771
+ const newMemberIds = new Set();
10772
+ for (const builder of builders) {
10773
+ builder.reconcileAdditions(newMemberIds);
10774
+ }
10775
+ for (const builder of builders) {
10776
+ builder.reconcileRemovals(newMemberIds);
10777
+ }
10778
+ let root;
10779
+ union.forEach((node) => (root || (root = node.root)));
10780
+ for (const builder of builders)
10781
+ root || (root = builder.getRoot());
10782
+ root?.didUpdate();
10783
+ /**
10784
+ * The root flushes the update on a microtask, synchronously
10785
+ * processing the frame that creates the layout animations. Collect
10786
+ * them in a later microtask step of the same pass.
10787
+ */
10788
+ microtask.render(() => {
10789
+ for (const builder of builders)
10790
+ builder.finalize();
10791
+ });
10792
+ };
10793
+ updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
10794
+ }
10043
10795
  class LayoutAnimationBuilder {
10044
10796
  constructor(scope, updateDom, defaultOptions) {
10045
- this.sharedTransitions = new Map();
10046
- this.notifyReady = noop;
10047
- this.rejectReady = noop;
10048
10797
  this.scope = scope;
10049
10798
  this.updateDom = updateDom;
10050
10799
  this.defaultOptions = defaultOptions;
10800
+ this.sharedTransitions = new Map();
10801
+ this.notifyReady = () => { };
10802
+ this.rejectReady = () => { };
10803
+ this.tracked = new Map();
10804
+ this.restorePoints = new Map();
10051
10805
  this.readyPromise = new Promise((resolve, reject) => {
10052
10806
  this.notifyReady = resolve;
10053
10807
  this.rejectReady = reject;
10054
10808
  });
10055
- frame.postRender(() => {
10056
- this.start().then(this.notifyReady).catch(this.rejectReady);
10057
- });
10809
+ if (!pendingBuilders) {
10810
+ pendingBuilders = [];
10811
+ queueMicrotask(flushPendingBuilders);
10812
+ }
10813
+ pendingBuilders.push(this);
10058
10814
  }
10059
10815
  shared(id, transition) {
10060
10816
  this.sharedTransitions.set(id, transition);
@@ -10063,114 +10819,107 @@
10063
10819
  then(resolve, reject) {
10064
10820
  return this.readyPromise.then(resolve, reject);
10065
10821
  }
10066
- async start() {
10067
- const beforeElements = collectLayoutElements(this.scope);
10068
- const beforeRecords = this.buildRecords(beforeElements);
10069
- beforeRecords.forEach(({ projection }) => {
10070
- const hasCurrentAnimation = Boolean(projection.currentAnimation);
10071
- const isSharedLayout = Boolean(projection.options.layoutId);
10072
- if (hasCurrentAnimation && isSharedLayout) {
10073
- const snapshot = snapshotFromTarget(projection);
10074
- if (snapshot) {
10075
- projection.snapshot = snapshot;
10076
- }
10077
- else if (projection.snapshot) {
10078
- projection.snapshot = undefined;
10079
- }
10080
- }
10081
- else if (projection.snapshot &&
10082
- (projection.currentAnimation || projection.isProjecting())) {
10083
- projection.snapshot = undefined;
10084
- }
10085
- projection.isPresent = true;
10086
- projection.willUpdate();
10822
+ transitionFor(element) {
10823
+ const layoutId = element.getAttribute("data-layout-id");
10824
+ return ((layoutId && this.sharedTransitions.get(layoutId)) ||
10825
+ this.defaultOptions);
10826
+ }
10827
+ adopt(element, node) {
10828
+ this.tracked.set(element, node);
10829
+ this.restorePoints.set(element, {
10830
+ parent: element.parentElement,
10831
+ next: element.nextSibling,
10087
10832
  });
10088
- await this.updateDom();
10089
- const afterElements = collectLayoutElements(this.scope);
10090
- const afterRecords = this.buildRecords(afterElements);
10091
- this.handleExitingElements(beforeRecords, afterRecords);
10092
- afterRecords.forEach(({ projection }) => {
10093
- const instance = projection.instance;
10094
- const resumeFromInstance = projection.resumeFrom
10095
- ?.instance;
10096
- if (!instance || !resumeFromInstance)
10097
- return;
10098
- if (!("style" in instance))
10099
- return;
10100
- const currentTransform = instance.style.transform;
10101
- const resumeFromTransform = resumeFromInstance.style.transform;
10102
- if (currentTransform &&
10103
- resumeFromTransform &&
10104
- currentTransform === resumeFromTransform) {
10105
- instance.style.transform = "";
10106
- instance.style.transformOrigin = "";
10833
+ }
10834
+ collectTargets() {
10835
+ return collectLayoutElements(this.scope);
10836
+ }
10837
+ runUpdate() {
10838
+ try {
10839
+ const result = this.updateDom();
10840
+ if (result && typeof result.then === "function") {
10841
+ return result.then(undefined, (error) => {
10842
+ this.updateError = error;
10843
+ });
10107
10844
  }
10108
- });
10109
- afterRecords.forEach(({ projection }) => {
10110
- projection.isPresent = true;
10111
- });
10112
- const root = getProjectionRoot(afterRecords, beforeRecords);
10113
- root?.didUpdate();
10114
- await new Promise((resolve) => {
10115
- frame.postRender(() => resolve());
10116
- });
10117
- const animations = collectAnimations(afterRecords);
10118
- const animation = new GroupAnimation(animations);
10119
- return animation;
10845
+ }
10846
+ catch (error) {
10847
+ this.updateError = error;
10848
+ }
10849
+ return undefined;
10120
10850
  }
10121
- buildRecords(elements) {
10122
- const records = [];
10123
- const recordMap = new Map();
10124
- for (const element of elements) {
10125
- const parentRecord = findParentRecord(element, recordMap, this.scope);
10126
- const { layout, layoutId } = readLayoutAttributes(element);
10127
- const override = layoutId
10128
- ? this.sharedTransitions.get(layoutId)
10129
- : undefined;
10130
- const transition = override || this.defaultOptions;
10131
- const record = getOrCreateRecord(element, parentRecord?.projection, {
10132
- layout,
10133
- layoutId,
10134
- animationType: typeof layout === "string" ? layout : "both",
10135
- transition: transition,
10136
- });
10137
- recordMap.set(element, record);
10138
- records.push(record);
10851
+ reconcileAdditions(newMemberIds) {
10852
+ for (const element of collectLayoutElements(this.scope)) {
10853
+ if (this.tracked.has(element))
10854
+ continue;
10855
+ const node = prepareNode(element, this.transitionFor(element));
10856
+ this.adopt(element, node);
10857
+ node.options.layoutId && newMemberIds.add(node.options.layoutId);
10139
10858
  }
10140
- return records;
10141
10859
  }
10142
- handleExitingElements(beforeRecords, afterRecords) {
10143
- const afterElementsSet = new Set(afterRecords.map((record) => record.element));
10144
- beforeRecords.forEach((record) => {
10145
- if (afterElementsSet.has(record.element))
10860
+ reconcileRemovals(newMemberIds) {
10861
+ this.tracked.forEach((node, element) => {
10862
+ if (element.isConnected)
10146
10863
  return;
10147
- // For shared layout elements, relegate to set up resumeFrom
10148
- // so the remaining element animates from this position
10149
- if (record.projection.options.layoutId) {
10150
- record.projection.isPresent = false;
10151
- record.projection.relegate();
10152
- }
10153
- record.visualElement.unmount();
10154
- visualElementStore.delete(record.element);
10864
+ const restore = this.restorePoints.get(element);
10865
+ this.restorePoints.delete(element);
10866
+ const { layoutId } = node.options;
10867
+ const stack = node.getStack();
10868
+ const hasSurvivor = stack &&
10869
+ stack.members.some((member) => member !== node &&
10870
+ member.instance
10871
+ ?.isConnected);
10872
+ /**
10873
+ * A removed lead with a surviving stack member - and no
10874
+ * replacement member added this commit - runs an exit
10875
+ * crossfade: restore the element to its old position in the
10876
+ * DOM, relegate it and let the survivor take over. It's
10877
+ * removed again once the animation completes.
10878
+ */
10879
+ if (layoutId &&
10880
+ node.isLead() &&
10881
+ hasSurvivor &&
10882
+ !newMemberIds.has(layoutId)) {
10883
+ if (restore && restore.parent.isConnected) {
10884
+ restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
10885
+ ? restore.next
10886
+ : null);
10887
+ node.isPresent = false;
10888
+ node.setOptions({
10889
+ onExitComplete: () => {
10890
+ element.remove();
10891
+ dropNode(element, node);
10892
+ },
10893
+ });
10894
+ if (node.relegate())
10895
+ return;
10896
+ element.remove();
10897
+ }
10898
+ }
10899
+ dropNode(element, node);
10900
+ this.tracked.delete(element);
10155
10901
  });
10156
- // Clear resumeFrom on EXISTING nodes that point to unmounted projections
10157
- // This prevents crossfade animation when the source element was removed entirely
10158
- // But preserve resumeFrom for NEW nodes so they can animate from the old position
10159
- // Also preserve resumeFrom for lead nodes that were just promoted via relegate
10160
- const beforeElementsSet = new Set(beforeRecords.map((record) => record.element));
10161
- afterRecords.forEach(({ element, projection }) => {
10162
- if (beforeElementsSet.has(element) &&
10163
- projection.resumeFrom &&
10164
- !projection.resumeFrom.instance &&
10165
- !projection.isLead()) {
10166
- projection.resumeFrom = undefined;
10167
- projection.snapshot = undefined;
10902
+ }
10903
+ getRoot() {
10904
+ let root;
10905
+ this.tracked.forEach((node) => (root || (root = node.root)));
10906
+ return root;
10907
+ }
10908
+ finalize() {
10909
+ if (this.updateError) {
10910
+ this.rejectReady(this.updateError);
10911
+ return;
10912
+ }
10913
+ const animations = new Set();
10914
+ this.tracked.forEach((node) => {
10915
+ if (node.instance && node.currentAnimation) {
10916
+ animations.add(node.currentAnimation);
10168
10917
  }
10169
10918
  });
10919
+ this.notifyReady(new GroupAnimation([...animations]));
10170
10920
  }
10171
10921
  }
10172
10922
  function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
10173
- // animateLayout(updateDom)
10174
10923
  if (typeof scopeOrUpdateDom === "function") {
10175
10924
  return {
10176
10925
  scope: document,
@@ -10178,107 +10927,15 @@
10178
10927
  defaultOptions: updateDomOrOptions,
10179
10928
  };
10180
10929
  }
10181
- // animateLayout(scope, updateDom, options?)
10182
- const elements = resolveElements(scopeOrUpdateDom);
10183
- const scope = elements[0] || document;
10930
+ const scope = scopeOrUpdateDom instanceof Document
10931
+ ? scopeOrUpdateDom
10932
+ : resolveElements(scopeOrUpdateDom)[0] ?? document;
10184
10933
  return {
10185
10934
  scope,
10186
10935
  updateDom: updateDomOrOptions,
10187
10936
  defaultOptions: options,
10188
10937
  };
10189
10938
  }
10190
- function collectLayoutElements(scope) {
10191
- const elements = Array.from(scope.querySelectorAll(layoutSelector));
10192
- if (scope instanceof Element && scope.matches(layoutSelector)) {
10193
- if (!elements.includes(scope)) {
10194
- elements.unshift(scope);
10195
- }
10196
- }
10197
- return elements;
10198
- }
10199
- function readLayoutAttributes(element) {
10200
- const layoutId = element.getAttribute("data-layout-id") || undefined;
10201
- const rawLayout = element.getAttribute("data-layout");
10202
- let layout;
10203
- if (rawLayout === "" || rawLayout === "true") {
10204
- layout = true;
10205
- }
10206
- else if (rawLayout) {
10207
- layout = rawLayout;
10208
- }
10209
- return {
10210
- layout,
10211
- layoutId,
10212
- };
10213
- }
10214
- function createVisualState() {
10215
- return {
10216
- latestValues: {},
10217
- renderState: {
10218
- transform: {},
10219
- transformOrigin: {},
10220
- style: {},
10221
- vars: {},
10222
- },
10223
- };
10224
- }
10225
- function getOrCreateRecord(element, parentProjection, projectionOptions) {
10226
- const existing = visualElementStore.get(element);
10227
- const visualElement = existing ??
10228
- new HTMLVisualElement({
10229
- props: {},
10230
- presenceContext: null,
10231
- visualState: createVisualState(),
10232
- }, { allowProjection: true });
10233
- if (!existing || !visualElement.projection) {
10234
- visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
10235
- }
10236
- visualElement.projection.setOptions({
10237
- ...projectionOptions,
10238
- visualElement,
10239
- });
10240
- if (!visualElement.current) {
10241
- visualElement.mount(element);
10242
- }
10243
- else if (!visualElement.projection.instance) {
10244
- // Mount projection if VisualElement is already mounted but projection isn't
10245
- // This happens when animate() was called before animateLayout()
10246
- visualElement.projection.mount(element);
10247
- }
10248
- if (!existing) {
10249
- visualElementStore.set(element, visualElement);
10250
- }
10251
- return {
10252
- element,
10253
- visualElement,
10254
- projection: visualElement.projection,
10255
- };
10256
- }
10257
- function findParentRecord(element, recordMap, scope) {
10258
- let parent = element.parentElement;
10259
- while (parent) {
10260
- const record = recordMap.get(parent);
10261
- if (record)
10262
- return record;
10263
- if (parent === scope)
10264
- break;
10265
- parent = parent.parentElement;
10266
- }
10267
- return undefined;
10268
- }
10269
- function getProjectionRoot(afterRecords, beforeRecords) {
10270
- const record = afterRecords[0] || beforeRecords[0];
10271
- return record?.projection.root;
10272
- }
10273
- function collectAnimations(afterRecords) {
10274
- const animations = new Set();
10275
- afterRecords.forEach((record) => {
10276
- const animation = record.projection.currentAnimation;
10277
- if (animation)
10278
- animations.add(animation);
10279
- });
10280
- return Array.from(animations);
10281
- }
10282
10939
 
10283
10940
  /**
10284
10941
  * @deprecated
@@ -11279,7 +11936,7 @@
11279
11936
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
11280
11937
  function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
11281
11938
  if (!container)
11282
- return noop$1;
11939
+ return noop;
11283
11940
  let containerHandlers = onScrollHandlers.get(container);
11284
11941
  /**
11285
11942
  * Get the onScroll handlers for this container.
@@ -11563,7 +12220,7 @@
11563
12220
 
11564
12221
  function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
11565
12222
  if (!container)
11566
- return noop$1;
12223
+ return noop;
11567
12224
  const optionsWithDefaults = { axis, container, ...options };
11568
12225
  return typeof onScroll === "function"
11569
12226
  ? attachToFunction(onScroll, optionsWithDefaults)
@@ -11643,7 +12300,6 @@
11643
12300
  exports.ViewTransitionBuilder = ViewTransitionBuilder;
11644
12301
  exports.VisualElement = VisualElement;
11645
12302
  exports.acceleratedValues = acceleratedValues;
11646
- exports.activeAnimations = activeAnimations;
11647
12303
  exports.addAttrValue = addAttrValue;
11648
12304
  exports.addDomEvent = addDomEvent;
11649
12305
  exports.addScaleCorrector = addScaleCorrector;
@@ -11842,7 +12498,7 @@
11842
12498
  exports.motionValue = motionValue;
11843
12499
  exports.moveItem = moveItem;
11844
12500
  exports.nodeGroup = nodeGroup;
11845
- exports.noop = noop$1;
12501
+ exports.noop = noop;
11846
12502
  exports.number = number;
11847
12503
  exports.numberValueTypes = numberValueTypes;
11848
12504
  exports.observeTimeline = observeTimeline;