motion 12.40.0 → 12.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -79,7 +79,7 @@
79
79
  }
80
80
 
81
81
  /*#__NO_SIDE_EFFECTS__*/
82
- const noop$1 = (any) => any;
82
+ const noop = (any) => any;
83
83
 
84
84
  /**
85
85
  * Pipe
@@ -216,7 +216,7 @@
216
216
  function cubicBezier(mX1, mY1, mX2, mY2) {
217
217
  // If this is a linear gradient, return linear easing
218
218
  if (mX1 === mY1 && mX2 === mY2)
219
- return noop$1;
219
+ return noop;
220
220
  const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
221
221
  // If animation is at start/end, return t without easing
222
222
  return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
@@ -277,7 +277,7 @@
277
277
  const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
278
278
 
279
279
  const easingLookup = {
280
- linear: noop$1,
280
+ linear: noop,
281
281
  easeIn,
282
282
  easeInOut,
283
283
  easeOut,
@@ -318,12 +318,7 @@
318
318
  "postRender", // Compute
319
319
  ];
320
320
 
321
- const statsBuffer = {
322
- value: null,
323
- addProjectionMetrics: null,
324
- };
325
-
326
- function createRenderStep(runNextFrame, stepName) {
321
+ function createRenderStep(runNextFrame) {
327
322
  /**
328
323
  * We create and reuse two queues, one to queue jobs for the current frame
329
324
  * and one for the next. We reuse to avoid triggering GC after x frames.
@@ -345,13 +340,11 @@
345
340
  timestamp: 0.0,
346
341
  isProcessing: false,
347
342
  };
348
- let numCalls = 0;
349
343
  function triggerCallback(callback) {
350
344
  if (toKeepAlive.has(callback)) {
351
345
  step.schedule(callback);
352
346
  runNextFrame();
353
347
  }
354
- numCalls++;
355
348
  callback(latestFrameData);
356
349
  }
357
350
  const step = {
@@ -394,13 +387,6 @@
394
387
  nextFrame = prevFrame;
395
388
  // Execute this frame
396
389
  thisFrame.forEach(triggerCallback);
397
- /**
398
- * If we're recording stats then
399
- */
400
- if (stepName && statsBuffer.value) {
401
- statsBuffer.value.frameloop[stepName].push(numCalls);
402
- }
403
- numCalls = 0;
404
390
  // Clear the frame so no callbacks remain. This is to avoid
405
391
  // memory leaks should this render step not run for a while.
406
392
  thisFrame.clear();
@@ -425,7 +411,7 @@
425
411
  };
426
412
  const flagRunNextFrame = () => (runNextFrame = true);
427
413
  const steps = stepsOrder.reduce((acc, key) => {
428
- acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
414
+ acc[key] = createRenderStep(flagRunNextFrame);
429
415
  return acc;
430
416
  }, {});
431
417
  const { setup, read, resolveKeyframes, preUpdate, update, preRender, render, postRender, } = steps;
@@ -481,7 +467,7 @@
481
467
  return { schedule, cancel, state, steps };
482
468
  }
483
469
 
484
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop$1, true);
470
+ const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
485
471
 
486
472
  let now;
487
473
  function clearTime() {
@@ -510,12 +496,6 @@
510
496
  },
511
497
  };
512
498
 
513
- const activeAnimations = {
514
- layout: 0,
515
- mainThread: 0,
516
- waapi: 0,
517
- };
518
-
519
499
  const checkStringStartsWith = (token) => (key) => typeof key === "string" && key.startsWith(token);
520
500
  const isCSSVariableName =
521
501
  /*@__PURE__*/ checkStringStartsWith("--");
@@ -1503,7 +1483,7 @@
1503
1483
  for (let i = 0; i < numMixers; i++) {
1504
1484
  let mixer = mixerFactory(output[i], output[i + 1]);
1505
1485
  if (ease) {
1506
- const easingFunction = Array.isArray(ease) ? ease[i] || noop$1 : ease;
1486
+ const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
1507
1487
  mixer = pipe(easingFunction, mixer);
1508
1488
  }
1509
1489
  mixers.push(mixer);
@@ -1717,7 +1697,6 @@
1717
1697
  this.teardown();
1718
1698
  this.options.onStop?.();
1719
1699
  };
1720
- activeAnimations.mainThread++;
1721
1700
  this.options = options;
1722
1701
  this.initAnimation();
1723
1702
  this.play();
@@ -2022,7 +2001,6 @@
2022
2001
  this.state = "idle";
2023
2002
  this.stopDriver();
2024
2003
  this.startTime = this.holdTime = null;
2025
- activeAnimations.mainThread--;
2026
2004
  }
2027
2005
  stopDriver() {
2028
2006
  if (!this.driver)
@@ -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,126 @@
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
+ * Resolve a selector/Element to elements and ensure each one carries a
5641
+ * `view-transition-name` we can target from script.
5642
+ *
5643
+ * Author-defined names are reused as-is. Elements that are unnamed (or use
5644
+ * the browser's `auto`/`match-element`, whose generated name is not exposed
5645
+ * to script) are given a unique generated name, set inline so it's captured,
5646
+ * and tracked in `assigned` for later cleanup.
5647
+ *
5648
+ * `registry` maps each Element to its name so the same element keeps its name
5649
+ * across both captures (before and after the update), which is what allows a
5650
+ * persistent element to animate as a single `group` layer.
5651
+ */
5652
+ function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = []) {
5653
+ const elements = resolveElements(definition);
5654
+ /**
5655
+ * The new end of a paired morph: give each element the matching name from
5656
+ * the old end (by index) so the two share one layer and morph. If the new
5657
+ * end resolves to *more* elements than the old end named, the extras have no
5658
+ * counterpart - give them a fresh name so they animate as newcomers rather
5659
+ * than being silently left unnamed. We return the names actually assigned
5660
+ * (sized to the resolved elements), not the raw `forcedNames`, so stagger
5661
+ * totals and the layer set stay in step with what's on the page.
5662
+ */
5663
+ if (forcedNames) {
5664
+ return elements.map((element, i) => {
5665
+ const existing = registry.get(element);
5666
+ if (existing)
5667
+ return existing;
5668
+ const name = forcedNames[i] ?? generatedName();
5669
+ element.style?.setProperty("view-transition-name", name);
5670
+ assigned.push(element);
5671
+ registry.set(element, name);
5672
+ tagClass(element, className, classed);
5673
+ return name;
5674
+ });
5675
+ }
5676
+ /**
5677
+ * Read every current name up front, before assigning any. Interleaving the
5678
+ * reads with the inline `setProperty` writes below would dirty styles
5679
+ * between reads and force a style recalc per element; batching the reads
5680
+ * keeps it to one. Elements already in the registry keep their name and
5681
+ * need no read.
5682
+ */
5683
+ const currentNames = elements.map((element) => registry.has(element)
5684
+ ? undefined
5685
+ : getComputedStyle(element).getPropertyValue("view-transition-name"));
5686
+ return elements.map((element, i) => {
5687
+ const existing = registry.get(element);
5688
+ if (existing)
5689
+ return existing;
5690
+ const current = currentNames[i];
5691
+ let name;
5692
+ if (current &&
5693
+ current !== "none" &&
5694
+ current !== "auto" &&
5695
+ current !== "match-element" &&
5696
+ !isGeneratedName(current)) {
5697
+ /**
5698
+ * The author already named this layer - target it as-is and leave
5699
+ * it to them to clean up. `auto`/`match-element` are overridden
5700
+ * because their generated name is not exposed to script, and a
5701
+ * stale `motion-view-*` (e.g. left by an interrupted transition) is
5702
+ * re-owned rather than adopted as an author name and leaked.
5703
+ */
5704
+ name = current;
5705
+ }
5706
+ else {
5707
+ name = generatedName();
5708
+ element.style?.setProperty("view-transition-name", name);
5709
+ assigned.push(element);
5710
+ }
5711
+ registry.set(element, name);
5712
+ tagClass(element, className, classed);
5713
+ return name;
5714
+ });
5715
+ }
5716
+ /**
5717
+ * Remove the `view-transition-name`s we generated and the
5718
+ * `view-transition-class`es we applied. Author-defined names are never touched
5719
+ * (they're not in `assigned`). Safe to call more than once (e.g. on both a
5720
+ * finished and an interrupted transition).
5721
+ */
5722
+ function releaseViewTransitionNames(assigned, classed = []) {
5723
+ for (const element of assigned) {
5724
+ element.style?.removeProperty("view-transition-name");
5725
+ }
5726
+ for (const element of classed) {
5727
+ element.style?.removeProperty("view-transition-class");
5728
+ }
5729
+ }
5730
+
5723
5731
  function chooseLayerType(valueName) {
5724
5732
  if (valueName === "layout")
5725
5733
  return "group";
5726
5734
  if (valueName === "enter" || valueName === "new")
5727
5735
  return "new";
5728
- if (valueName === "exit" || valueName === "old")
5729
- return "old";
5730
- return "group";
5736
+ return "old";
5731
5737
  }
5732
5738
 
5733
5739
  let pendingRules = {};
@@ -5762,21 +5768,21 @@
5762
5768
  };
5763
5769
 
5764
5770
  function getViewAnimationLayerInfo(pseudoElement) {
5765
- const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
5771
+ const match = pseudoElement.match(
5772
+ // `group-children` (nested transitions) before `group` so it wins.
5773
+ /::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
5766
5774
  if (!match)
5767
5775
  return null;
5768
5776
  return { layer: match[2], type: match[1] };
5769
5777
  }
5770
5778
 
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
5779
  function getViewAnimations() {
5779
- return document.getAnimations().filter(filterViewAnimations);
5780
+ return document.getAnimations().filter((animation) => {
5781
+ const { effect } = animation;
5782
+ return (!!effect &&
5783
+ effect.target === document.documentElement &&
5784
+ effect.pseudoElement?.startsWith("::view-transition"));
5785
+ });
5780
5786
  }
5781
5787
 
5782
5788
  function hasTarget(target, targets) {
@@ -5784,87 +5790,370 @@
5784
5790
  }
5785
5791
 
5786
5792
  const definitionNames = ["layout", "enter", "exit", "new", "old"];
5793
+ /**
5794
+ * The `ViewTransitionTarget` buckets driving each generated layer type, in
5795
+ * priority order - the inverse of `chooseLayerType`. The new view is driven by
5796
+ * `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
5797
+ * bucket; they follow the default layout timing.
5798
+ */
5799
+ const typeBuckets = {
5800
+ group: ["layout"],
5801
+ new: ["new", "enter"],
5802
+ old: ["old", "exit"],
5803
+ };
5804
+ /**
5805
+ * Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
5806
+ * `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
5807
+ * from 1. `enter` prefers the matching `exit` value over these (see below).
5808
+ */
5809
+ const ORIGIN_DEFAULTS = {
5810
+ new: { opacity: 0, scale: 0.85 },
5811
+ old: { opacity: 1, scale: 1 },
5812
+ };
5813
+ const cornerProps = [
5814
+ "borderTopLeftRadius",
5815
+ "borderTopRightRadius",
5816
+ "borderBottomRightRadius",
5817
+ "borderBottomLeftRadius",
5818
+ ];
5787
5819
  function startViewAnimation(builder) {
5788
- const { update, targets, options: defaultOptions } = builder;
5820
+ const { update, targets, resolveDefs, noCrop, pairs, classNames, options: defaultOptions, } = builder;
5789
5821
  if (!document.startViewTransition) {
5790
- return new Promise(async (resolve) => {
5822
+ // An async IIFE (not `new Promise(async )`) so a throwing/rejecting
5823
+ // update rejects this promise rather than leaving it unsettled.
5824
+ return (async () => {
5791
5825
  await update();
5792
- resolve(new GroupAnimation([]));
5793
- });
5826
+ return new GroupAnimation([]);
5827
+ })();
5794
5828
  }
5795
- // TODO: Go over existing targets and ensure they all have ids
5796
5829
  /**
5797
- * If we don't have any animations defined for the root target,
5798
- * remove it from being captured.
5830
+ * Resolve any selector/Element targets to layer names, assigning a
5831
+ * `view-transition-name` to each element as we go. We run this before the
5832
+ * update (so the elements are captured in the old snapshot) and again
5833
+ * after it (for the new snapshot). An element present in both keeps the
5834
+ * same name and animates as a single `group` layer.
5835
+ */
5836
+ const nameRegistry = new Map();
5837
+ const assigned = [];
5838
+ /**
5839
+ * Elements we tagged with a `view-transition-class` (via `.class()`),
5840
+ * tracked separately from `assigned` so cleanup removes the class without
5841
+ * ever stripping an author's own inline `view-transition-name`.
5799
5842
  */
5800
- if (!hasTarget("root", targets)) {
5801
- css.set(":root", {
5802
- "view-transition-name": "none",
5843
+ const classed = [];
5844
+ const layerTargets = new Map();
5845
+ const croppedNames = new Set();
5846
+ /**
5847
+ * Each layer's stagger position (index + total) within its subject, per
5848
+ * snapshot. Resolving against the snapshot the layer belongs to keeps
5849
+ * stagger correct when `update()` replaces the matched elements, and lets
5850
+ * us skip a layer that's absent from a snapshot (e.g. an exited element
5851
+ * has no `new` pseudo-element).
5852
+ */
5853
+ const layerStagger = new Map();
5854
+ /**
5855
+ * Names allocated for a paired subject in the old snapshot, replayed onto
5856
+ * its new-snapshot target so both ends share a layer and morph.
5857
+ */
5858
+ const pairNames = new Map();
5859
+ /**
5860
+ * The old (`from`) elements of each paired subject, so their names can be
5861
+ * transferred off before the new (`to`) elements inherit them.
5862
+ */
5863
+ const pairFrom = new Map();
5864
+ const resolveLayers = (phase) => {
5865
+ targets.forEach((target, definition) => {
5866
+ const className = classNames.get(definition);
5867
+ let names;
5868
+ if (definition === "root" || !resolveDefs.has(definition)) {
5869
+ names = [definition];
5870
+ }
5871
+ else if (pairs.has(definition)) {
5872
+ /**
5873
+ * Paired morph: name the old target in the old snapshot, then
5874
+ * force the same name(s) onto the new target in the new one, so
5875
+ * two different elements morph as a single layer.
5876
+ */
5877
+ if (phase === "old") {
5878
+ pairFrom.set(definition, resolveElements(definition));
5879
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
5880
+ pairNames.set(definition, names);
5881
+ }
5882
+ else {
5883
+ /**
5884
+ * Transfer the name(s) off the `from` elements before the
5885
+ * `to` elements inherit them. A `from` that survives into
5886
+ * the new snapshot (e.g. hidden with `visibility: hidden`
5887
+ * rather than removed) would otherwise keep the name and
5888
+ * collide - "duplicate view-transition-name".
5889
+ */
5890
+ for (const el of pairFrom.get(definition) ?? []) {
5891
+ el.style?.removeProperty("view-transition-name");
5892
+ /**
5893
+ * Drop the old end from the registry too, so the new
5894
+ * end alone supplies this name's `new` crop radii - we
5895
+ * neither re-measure nor get ordered by a stale element.
5896
+ */
5897
+ nameRegistry.delete(el);
5898
+ }
5899
+ names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed);
5900
+ }
5901
+ }
5902
+ else {
5903
+ names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed);
5904
+ }
5905
+ const cropped = definition !== "root" && !noCrop.has(definition);
5906
+ names.forEach((name, index) => {
5907
+ /**
5908
+ * If two subjects resolve to the same element, merge their
5909
+ * definitions so neither subject's animations are dropped.
5910
+ */
5911
+ const existing = layerTargets.get(name);
5912
+ layerTargets.set(name, existing && existing !== target
5913
+ ? { ...existing, ...target }
5914
+ : target);
5915
+ if (cropped)
5916
+ croppedNames.add(name);
5917
+ const stagger = layerStagger.get(name) ?? {};
5918
+ stagger[phase] = [index, names.length];
5919
+ layerStagger.set(name, stagger);
5920
+ });
5803
5921
  });
5804
- }
5922
+ };
5923
+ /**
5924
+ * The stagger index/total for a layer, resolved against the snapshot it
5925
+ * belongs to. Returns index -1 when the layer is absent from that snapshot
5926
+ * so the caller can skip a pseudo-element that doesn't exist.
5927
+ */
5928
+ const staggerPosition = (name, type) => {
5929
+ const stagger = layerStagger.get(name);
5930
+ const position = type === "old"
5931
+ ? stagger?.old
5932
+ : type === "new"
5933
+ ? stagger?.new
5934
+ : // group / group-children / image-pair persist across both.
5935
+ stagger?.new ?? stagger?.old;
5936
+ return position ?? [-1, 1];
5937
+ };
5938
+ /**
5939
+ * Merge default + per-layer transition options for a generated layer and
5940
+ * resolve any stagger/delay function against this element's position. Used
5941
+ * by both the morph-retiming and crop corner-radius passes.
5942
+ */
5943
+ const resolveLayerTransition = (target, type, transitionName, index, total) => {
5944
+ const transition = mergeTransition(getValueTransition$1(defaultOptions, transitionName), getValueTransition$1((layerOptions(target, type) ?? {}), transitionName));
5945
+ if (typeof transition.delay === "function") {
5946
+ transition.delay = transition.delay(index, total);
5947
+ }
5948
+ return transition;
5949
+ };
5950
+ /**
5951
+ * Measured corner radii per cropped layer, so the clip can animate each
5952
+ * corner between the old and new elements. Per-corner (rather than the
5953
+ * shorthand) so mismatched/individual radii interpolate cleanly.
5954
+ */
5955
+ const cropRadii = new Map();
5956
+ const recordRadii = (style, name, phase) => {
5957
+ const corners = {};
5958
+ for (const corner of cornerProps)
5959
+ corners[corner] = style[corner];
5960
+ const entry = cropRadii.get(name) ?? {};
5961
+ entry[phase] = corners;
5962
+ cropRadii.set(name, entry);
5963
+ };
5805
5964
  /**
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.
5965
+ * Cropped layers all come from `.add()`, so their elements are in the
5966
+ * registry - read each one's corner radii directly. For a paired morph both
5967
+ * ends share a name; the new-snapshot element is registered last, so it
5968
+ * wins the `new` reading (and the old end the `old` reading).
5969
+ */
5970
+ const measureCrop = (phase) => {
5971
+ if (!croppedNames.size)
5972
+ return;
5973
+ nameRegistry.forEach((name, element) => {
5974
+ if (croppedNames.has(name)) {
5975
+ recordRadii(getComputedStyle(element), name, phase);
5976
+ }
5977
+ });
5978
+ };
5979
+ /**
5980
+ * Write the persistent view-transition CSS: suppress root capture when the
5981
+ * root has no animations of its own; force linear timing (baked into the
5982
+ * keyframes, so we can retime later via updateTiming); and clip +
5983
+ * object-fit: cover every cropped morph (the UA default overflows on
5984
+ * aspect-ratio change), with an animated border-radius added below.
5809
5985
  *
5810
- * This allows us to set easing via updateTiming - which can be changed.
5986
+ * `css.commit` replaces rather than appends, so we re-set the full rule set
5987
+ * each call - the second call (in the update callback) then picks up cropped
5988
+ * layers that only exist in the new snapshot.
5811
5989
  */
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
5817
- });
5818
- transition.finished.finally(() => {
5990
+ const commitViewCSS = () => {
5991
+ if (!hasTarget("root", targets)) {
5992
+ css.set(":root", { "view-transition-name": "none" });
5993
+ }
5994
+ css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
5995
+ croppedNames.forEach((name) => {
5996
+ css.set(`::view-transition-group(${name})`, { overflow: "clip" });
5997
+ css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
5998
+ });
5999
+ css.commit(); // Write
6000
+ };
6001
+ const cleanup = () => {
6002
+ releaseViewTransitionNames(assigned, classed);
5819
6003
  css.remove(); // Write
5820
- });
5821
- return new Promise((resolve) => {
5822
- transition.ready.then(() => {
6004
+ };
6005
+ const callback = async () => {
6006
+ await update();
6007
+ /**
6008
+ * Re-resolve so elements created by the update are named for the new
6009
+ * snapshot, then measure the cropped layers' new border-radius.
6010
+ */
6011
+ const croppedBefore = croppedNames.size;
6012
+ resolveLayers("new");
6013
+ measureCrop("new");
6014
+ /**
6015
+ * Re-commit the crop CSS only if the new snapshot introduced cropped
6016
+ * layers, so a layer that exists only in the new snapshot is clipped
6017
+ * too - without forcing a redundant style write on the common path.
6018
+ */
6019
+ if (croppedNames.size > croppedBefore)
6020
+ commitViewCSS();
6021
+ };
6022
+ let transition;
6023
+ try {
6024
+ resolveLayers("old");
6025
+ measureCrop("old");
6026
+ commitViewCSS();
6027
+ transition = document.startViewTransition(callback);
6028
+ }
6029
+ catch (error) {
6030
+ /**
6031
+ * The prelude writes inline names before the transition exists. If it
6032
+ * throws (e.g. startViewTransition rejects in a bad UA state), release
6033
+ * them so we neither leak DOM state nor stall the queue on a promise
6034
+ * that never settles - hand back a rejection it can advance past.
6035
+ */
6036
+ cleanup();
6037
+ return Promise.reject(error);
6038
+ }
6039
+ transition.finished.finally(cleanup);
6040
+ return new Promise((resolve, reject) => {
6041
+ transition.ready
6042
+ .then(() => {
5823
6043
  const generatedViewAnimations = getViewAnimations();
5824
6044
  const animations = [];
5825
6045
  /**
5826
6046
  * Create animations for each of our explicitly-defined subjects.
6047
+ * `opacityAnimated` additionally tracks which `${name}:${type}`
6048
+ * we faded, so we can keep the UA `plus-lighter` blend only for a
6049
+ * genuine opacity crossfade (both sides fading) and drop it for a
6050
+ * slide/transform, where additive compositing would flash bright.
5827
6051
  */
5828
- targets.forEach((definition, target) => {
5829
- // TODO: If target is not "root", resolve elements
5830
- // and iterate over each
6052
+ const explicitlyAnimated = new Set();
6053
+ const opacityAnimated = new Set();
6054
+ layerTargets.forEach((target, name) => {
6055
+ const stagger = layerStagger.get(name);
6056
+ /**
6057
+ * Presence: `enter` only fires for a pure newcomer (a new
6058
+ * view with no old), `exit` only for a pure leaver. A
6059
+ * survivor (both) gets neither - it just morphs.
6060
+ */
6061
+ const enterApplies = !!stagger?.new && !stagger?.old;
6062
+ const exitApplies = !!stagger?.old && !stagger?.new;
5831
6063
  for (const key of definitionNames) {
5832
- if (!definition[key])
6064
+ if (!target[key])
6065
+ continue;
6066
+ if (key === "enter" && !enterApplies)
6067
+ continue;
6068
+ if (key === "exit" && !exitApplies)
6069
+ continue;
6070
+ const type = chooseLayerType(key);
6071
+ const [index, total] = staggerPosition(name, type);
6072
+ // Skip a layer absent from its snapshot.
6073
+ if (index === -1)
5833
6074
  continue;
5834
- const { keyframes, options } = definition[key];
6075
+ const { keyframes, options } = target[key];
5835
6076
  for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
5836
- if (!valueKeyframes)
6077
+ // Skip only missing values - `0` (e.g. opacity: 0)
6078
+ // is valid and must reach the from-value inference.
6079
+ if (valueKeyframes == null)
5837
6080
  continue;
5838
- const valueOptions = {
5839
- ...getValueTransition$1(defaultOptions, valueName),
5840
- ...getValueTransition$1(options, valueName),
5841
- };
5842
- const type = chooseLayerType(key);
5843
6081
  /**
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.
6082
+ * The view path hands keyframes straight to WAAPI,
6083
+ * so Motion's `x`/`y` shorthands (compiled to
6084
+ * `transform` only via the value pipeline) have no
6085
+ * effect. Warn and skip - use `transform`/`translate`.
6086
+ */
6087
+ if (valueName === "x" || valueName === "y") {
6088
+ warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
6089
+ continue;
6090
+ }
6091
+ /**
6092
+ * enter/exit win over new/old on a shared property -
6093
+ * skip it here when the gated bucket also defines it.
5846
6094
  */
5847
- if (valueName === "opacity" &&
5848
- !Array.isArray(valueKeyframes)) {
5849
- const initialValue = type === "new" ? 0 : 1;
5850
- valueKeyframes = [initialValue, valueKeyframes];
6095
+ if (key === "new" &&
6096
+ enterApplies &&
6097
+ target.enter?.keyframes[valueName] != null) {
6098
+ continue;
6099
+ }
6100
+ if (key === "old" &&
6101
+ exitApplies &&
6102
+ target.exit?.keyframes[valueName] != null) {
6103
+ continue;
6104
+ }
6105
+ const valueOptions = mergeTransition(getValueTransition$1(defaultOptions, valueName), getValueTransition$1(options, valueName));
6106
+ /**
6107
+ * Infer an origin for a single-value keyframe. An
6108
+ * `enter` mirrors the matching `exit` value (a
6109
+ * defined exit reverses into the enter for free);
6110
+ * otherwise the per-type default (opacity 0/1, scale
6111
+ * 0.85). No default -> left as-is (animates from the
6112
+ * live value).
6113
+ *
6114
+ * `new`/`old` fire for survivors too, where only the
6115
+ * opacity crossfade default applies - a transform
6116
+ * default like scale 0.85 would pop a persisting
6117
+ * element, so gate it on the layer actually
6118
+ * entering/leaving.
6119
+ */
6120
+ if (!Array.isArray(valueKeyframes)) {
6121
+ const exitValue = key === "enter"
6122
+ ? target.exit?.keyframes[valueName]
6123
+ : undefined;
6124
+ const allowDefault = valueName === "opacity" ||
6125
+ (type === "new" ? enterApplies : exitApplies);
6126
+ const from = exitValue != null
6127
+ ? Array.isArray(exitValue)
6128
+ ? exitValue[exitValue.length - 1]
6129
+ : exitValue
6130
+ : allowDefault
6131
+ ? ORIGIN_DEFAULTS[type]?.[valueName]
6132
+ : undefined;
6133
+ if (from !== undefined) {
6134
+ valueKeyframes = [from, valueKeyframes];
6135
+ }
5851
6136
  }
5852
6137
  /**
5853
- * Resolve stagger function if provided.
6138
+ * Resolve stagger function if provided, per element
6139
+ * across this subject's resolved layers.
5854
6140
  */
5855
6141
  if (typeof valueOptions.delay === "function") {
5856
- valueOptions.delay = valueOptions.delay(0, 1);
6142
+ valueOptions.delay = valueOptions.delay(index, total);
5857
6143
  }
5858
6144
  valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
5859
6145
  valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
5860
- const animation = new NativeAnimation({
6146
+ animations.push(new NativeAnimation({
5861
6147
  ...valueOptions,
5862
6148
  element: document.documentElement,
5863
6149
  name: valueName,
5864
- pseudoElement: `::view-transition-${type}(${target})`,
6150
+ pseudoElement: `::view-transition-${type}(${name})`,
5865
6151
  keyframes: valueKeyframes,
5866
- });
5867
- animations.push(animation);
6152
+ }));
6153
+ explicitlyAnimated.add(`${name}:${type}`);
6154
+ if (valueName === "opacity") {
6155
+ opacityAnimated.add(`${name}:${type}`);
6156
+ }
5868
6157
  }
5869
6158
  }
5870
6159
  });
@@ -5883,45 +6172,161 @@
5883
6172
  const name = getViewAnimationLayerInfo(pseudoElement);
5884
6173
  if (!name)
5885
6174
  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();
6175
+ const targetDefinition = layerTargets.get(name.layer);
6176
+ /**
6177
+ * We built our own animation for this layer, so drop the
6178
+ * browser-generated fade we're replacing. The UA
6179
+ * `plus-lighter` blend is a *separate* generated animation on
6180
+ * the same pseudo (it sets `mix-blend-mode` in its keyframes):
6181
+ * keep it *only* for a true opacity crossfade - both sides
6182
+ * fading - so a symmetric crossfade composites without
6183
+ * darkening, but a slide/transform (where both layers stay
6184
+ * opaque and overlap) doesn't flash bright from the addition.
6185
+ */
6186
+ if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
6187
+ const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
6188
+ opacityAnimated.has(`${name.layer}:old`);
6189
+ if (isCrossfade &&
6190
+ effect
6191
+ .getKeyframes()
6192
+ .some((keyframe) => keyframe.mixBlendMode)) {
6193
+ animations.push(new NativeAnimationWrapper(animation));
6194
+ }
6195
+ else {
6196
+ animation.cancel();
6197
+ }
6198
+ continue;
5917
6199
  }
6200
+ /**
6201
+ * Otherwise retime the browser-generated animation to
6202
+ * Motion's timing. This auto-enables the layout (group)
6203
+ * morph for any resolved/named target, and applies the
6204
+ * default timing to old/new layers we haven't explicitly
6205
+ * overridden.
6206
+ *
6207
+ * group + group-children both follow the layout timing so
6208
+ * the nesting container stays in sync with the morph.
6209
+ */
6210
+ /**
6211
+ * A survivor's old + new are the two halves of one
6212
+ * `plus-lighter` crossfade. They must share identical timing
6213
+ * (so their opacities stay mirrored and sum to 1 - else the
6214
+ * additive blend flashes bright wherever both are partly
6215
+ * visible) and fade linearly (the bounce belongs on the
6216
+ * group's geometry, not the opacity). So time them as the
6217
+ * group, rather than via their own - potentially staggered,
6218
+ * or enter/exit-derived - old/new options.
6219
+ */
6220
+ const stagger = layerStagger.get(name.layer);
6221
+ const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
6222
+ !!stagger?.old &&
6223
+ !!stagger?.new;
6224
+ const timingType = name.type.startsWith("group") || isMorphCrossfade
6225
+ ? "group"
6226
+ : name.type;
6227
+ const [index, total] = staggerPosition(name.layer, timingType);
6228
+ const transitionName = timingType === "group" ? "layout" : "";
6229
+ let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
6230
+ /**
6231
+ * The crossfade should resolve at the spring's *perceptual*
6232
+ * (visual) duration - the geometry can keep bouncing, but the
6233
+ * opacity shouldn't drag through the settle. So capture
6234
+ * `visualDuration` before `applyGeneratorOptions` replaces it
6235
+ * with the full overshoot duration, and use it for the fade.
6236
+ */
6237
+ const visualDuration = animationTransition.visualDuration;
6238
+ animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
6239
+ animationTransition =
6240
+ applyGeneratorOptions(animationTransition);
6241
+ const duration = isMorphCrossfade && visualDuration !== undefined
6242
+ ? secondsToMilliseconds(visualDuration)
6243
+ : animationTransition.duration;
6244
+ const easing = isMorphCrossfade
6245
+ ? "linear"
6246
+ : mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
6247
+ effect.updateTiming({
6248
+ delay: secondsToMilliseconds(animationTransition.delay ?? 0),
6249
+ duration,
6250
+ easing,
6251
+ });
6252
+ animations.push(new NativeAnimationWrapper(animation));
5918
6253
  }
6254
+ /**
6255
+ * Animate each cropped layer's clip corners between the old and
6256
+ * new elements, so a cropped morph keeps rounded corners
6257
+ * (handling individual per-corner radii).
6258
+ */
6259
+ cropRadii.forEach((radii, name) => {
6260
+ if (!radii.old && !radii.new)
6261
+ return;
6262
+ const target = layerTargets.get(name);
6263
+ const [index, total] = staggerPosition(name, "group");
6264
+ const radiusOptions = resolveLayerTransition(target, "group", "layout", index === -1 ? 0 : index, total);
6265
+ radiusOptions.duration && (radiusOptions.duration = secondsToMilliseconds(radiusOptions.duration));
6266
+ radiusOptions.delay && (radiusOptions.delay = secondsToMilliseconds(radiusOptions.delay));
6267
+ for (const corner of cornerProps) {
6268
+ // `||` (not `??`) so an empty measurement (e.g. an
6269
+ // un-rendered element) falls back rather than producing
6270
+ // an invalid keyframe.
6271
+ const from = radii.old?.[corner] || radii.new?.[corner] || "0px";
6272
+ const to = radii.new?.[corner] || radii.old?.[corner] || "0px";
6273
+ // Skip square corners - nothing to round.
6274
+ if (parseFloat(from) === 0 && parseFloat(to) === 0) {
6275
+ continue;
6276
+ }
6277
+ animations.push(new NativeAnimation({
6278
+ ...radiusOptions,
6279
+ element: document.documentElement,
6280
+ name: corner,
6281
+ pseudoElement: `::view-transition-group(${name})`,
6282
+ keyframes: [from, to],
6283
+ }));
6284
+ }
6285
+ });
5919
6286
  resolve(new GroupAnimation(animations));
5920
- });
6287
+ })
6288
+ .catch(() =>
6289
+ /**
6290
+ * `ready` rejects when the transition is skipped - no visual
6291
+ * change, or superseded by an interrupting transition. The DOM
6292
+ * update still applied, so settle with no animations rather than
6293
+ * surfacing it as an error to an awaiting caller. A genuine
6294
+ * failure in `update()` rejects `updateCallbackDone` (already
6295
+ * settled by now), so propagate that instead.
6296
+ */
6297
+ transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
5921
6298
  });
5922
6299
  }
5923
- function hasOpacity(target, key) {
5924
- return target?.[key]?.keyframes.opacity;
6300
+ /**
6301
+ * The options that should time a given generated layer type, so a retimed
6302
+ * group/old/new picks up any per-target transition the user provided. Checks
6303
+ * the type's buckets in priority order (e.g. `new` before `enter`).
6304
+ */
6305
+ function layerOptions(target, type) {
6306
+ for (const bucket of typeBuckets[type] ?? []) {
6307
+ const options = target?.[bucket]?.options;
6308
+ if (options)
6309
+ return options;
6310
+ }
6311
+ }
6312
+ /**
6313
+ * Merge a base transition (e.g. the default `options`) with a per-layer/value
6314
+ * override. An explicit `duration` on the override must win over an inherited
6315
+ * generator's own timing: a spring prefers `visualDuration`, and
6316
+ * `spring.applyToOptions` overwrites `duration` with the computed settle time -
6317
+ * so without this the override is silently discarded. Dropping the inherited
6318
+ * `type`/`visualDuration` makes the layer a plain tween of that duration, unless
6319
+ * it asked for its own generator `type`/`visualDuration`.
6320
+ */
6321
+ function mergeTransition(base, override) {
6322
+ const merged = { ...base, ...override };
6323
+ if (override.duration !== undefined) {
6324
+ if (override.visualDuration === undefined)
6325
+ delete merged.visualDuration;
6326
+ if (override.type === undefined)
6327
+ delete merged.type;
6328
+ }
6329
+ return merged;
5925
6330
  }
5926
6331
 
5927
6332
  let builders = [];
@@ -5935,10 +6340,16 @@
5935
6340
  function start(builder) {
5936
6341
  removeItem(builders, builder);
5937
6342
  current = builder;
5938
- startViewAnimation(builder).then((animation) => {
6343
+ startViewAnimation(builder)
6344
+ .then((animation) => {
5939
6345
  builder.notifyReady(animation);
5940
- animation.finished.finally(next);
5941
- });
6346
+ return animation.finished;
6347
+ })
6348
+ // A genuinely failed transition (a throwing update) rejects the
6349
+ // builder; a skipped/interrupted one resolves with no animations (see
6350
+ // start.ts). Either way, advance the queue - else later transitions hang.
6351
+ .catch((error) => builder.notifyReject(error))
6352
+ .finally(next);
5942
6353
  }
5943
6354
  function processQueue() {
5944
6355
  /**
@@ -5975,31 +6386,96 @@
5975
6386
  constructor(update, options = {}) {
5976
6387
  this.currentSubject = "root";
5977
6388
  this.targets = new Map();
5978
- this.notifyReady = noop$1;
5979
- this.readyPromise = new Promise((resolve) => {
6389
+ /**
6390
+ * Definitions that must be resolved to elements (and assigned a
6391
+ * `view-transition-name`) rather than treated as pre-named layers.
6392
+ */
6393
+ this.resolveDefs = new Set();
6394
+ /**
6395
+ * Subjects opted out of the default crop (clip + object-fit: cover +
6396
+ * animated corner radii) via `.crop(false)`.
6397
+ */
6398
+ this.noCrop = new Set();
6399
+ /**
6400
+ * Subjects paired with a different new-snapshot target (the second `.add()`
6401
+ * argument), so two distinct elements share one name and morph into each
6402
+ * other - a shared-element transition.
6403
+ */
6404
+ this.pairs = new Map();
6405
+ /**
6406
+ * A `view-transition-class` to apply to each subject's resolved elements,
6407
+ * so authors can target the generated layers from CSS by class rather than
6408
+ * the opaque generated name.
6409
+ */
6410
+ this.classNames = new Map();
6411
+ this.notifyReady = noop;
6412
+ this.notifyReject = noop;
6413
+ this.readyPromise = new Promise((resolve, reject) => {
5980
6414
  this.notifyReady = resolve;
6415
+ this.notifyReject = reject;
5981
6416
  });
5982
6417
  this.update = update;
5983
6418
  this.options = {
5984
6419
  interrupt: "wait",
5985
6420
  ...options,
5986
6421
  };
6422
+ // Avoid an unhandled rejection when a failed transition has no
6423
+ // `.then(_, reject)` handler attached (e.g. fire-and-forget).
6424
+ this.readyPromise.catch(noop);
5987
6425
  addToQueue(this);
5988
6426
  }
5989
- get(subject) {
6427
+ /**
6428
+ * Target elements resolved from a selector or Element, each assigned a
6429
+ * `view-transition-name` automatically.
6430
+ *
6431
+ * Passing a second target pairs them: the first is resolved in the old
6432
+ * snapshot and the second in the new, sharing one name so two *different*
6433
+ * elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
6434
+ * pass them the other way round to morph back.
6435
+ */
6436
+ add(subject, newSubject) {
5990
6437
  this.currentSubject = subject;
6438
+ this.resolveDefs.add(subject);
6439
+ if (newSubject !== undefined)
6440
+ this.pairs.set(subject, newSubject);
6441
+ // Register the subject so it participates (and gets an automatic
6442
+ // layout/morph animation) even without an explicit enter/exit/layout.
6443
+ if (!this.targets.has(subject))
6444
+ this.targets.set(subject, {});
5991
6445
  return this;
5992
6446
  }
5993
- layout(keyframes, options) {
5994
- this.updateTarget("layout", keyframes, options);
6447
+ /**
6448
+ * Morphs are clipped + `object-fit: cover` (and their corners animate)
6449
+ * by default. Call `.crop(false)` to opt this subject out and fall back
6450
+ * to the browser default (which overflows on aspect-ratio change).
6451
+ */
6452
+ crop(enabled = true) {
6453
+ enabled
6454
+ ? this.noCrop.delete(this.currentSubject)
6455
+ : this.noCrop.add(this.currentSubject);
5995
6456
  return this;
5996
6457
  }
5997
- new(keyframes, options) {
5998
- this.updateTarget("new", keyframes, options);
6458
+ /**
6459
+ * Tag this subject's generated layers with a `view-transition-class`, so
6460
+ * they can be targeted from CSS - `::view-transition-group(.name)`,
6461
+ * `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
6462
+ * - without the opaque generated `view-transition-name`. Because `.add()`
6463
+ * can match many elements, a shared class targets them all at once (and,
6464
+ * for a pair, both ends). The escape hatch for z-index / custom keyframes
6465
+ * on a morph layer.
6466
+ */
6467
+ class(name) {
6468
+ this.classNames.set(this.currentSubject, name);
5999
6469
  return this;
6000
6470
  }
6001
- old(keyframes, options) {
6002
- this.updateTarget("old", keyframes, options);
6471
+ /**
6472
+ * Set the transition for this subject's morph. The morph is enabled
6473
+ * automatically by `.add()`; this just customises its timing (duration,
6474
+ * easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
6475
+ * opts the page into the transition (the root crossfade).
6476
+ */
6477
+ layout(options = {}) {
6478
+ this.updateTarget("layout", {}, options);
6003
6479
  return this;
6004
6480
  }
6005
6481
  enter(keyframes, options) {
@@ -6010,9 +6486,21 @@
6010
6486
  this.updateTarget("exit", keyframes, options);
6011
6487
  return this;
6012
6488
  }
6013
- crossfade(options) {
6014
- this.updateTarget("enter", { opacity: 1 }, options);
6015
- this.updateTarget("exit", { opacity: 0 }, options);
6489
+ /**
6490
+ * Animate the new view directly, whether the element is appearing or
6491
+ * persisting (unlike `.enter()`, which only fires for a pure newcomer).
6492
+ * Pair with `.old()` for a crossfade or slide-through.
6493
+ */
6494
+ new(keyframes, options) {
6495
+ this.updateTarget("new", keyframes, options);
6496
+ return this;
6497
+ }
6498
+ /**
6499
+ * Animate the old view directly, whether the element is leaving or
6500
+ * persisting (unlike `.exit()`, which only fires for a pure leaver).
6501
+ */
6502
+ old(keyframes, options) {
6503
+ this.updateTarget("old", keyframes, options);
6016
6504
  return this;
6017
6505
  }
6018
6506
  updateTarget(target, keyframes, options = {}) {
@@ -6027,8 +6515,8 @@
6027
6515
  return this.readyPromise.then(resolve, reject);
6028
6516
  }
6029
6517
  }
6030
- function animateView(update, defaultOptions = {}) {
6031
- return new ViewTransitionBuilder(update, defaultOptions);
6518
+ function animateView(update, options = {}) {
6519
+ return new ViewTransitionBuilder(update, options);
6032
6520
  }
6033
6521
 
6034
6522
  const createAxisDelta = () => ({
@@ -8128,7 +8616,7 @@
8128
8616
  : values.borderRadius;
8129
8617
  }
8130
8618
  const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
8131
- const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop$1);
8619
+ const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
8132
8620
  function compress(min, max, easing) {
8133
8621
  return (p) => {
8134
8622
  // Could replace ifs with clamp
@@ -8148,7 +8636,7 @@
8148
8636
 
8149
8637
  function addDomEvent(target, eventName, handler, options = { passive: true }) {
8150
8638
  target.addEventListener(eventName, handler, options);
8151
- return () => target.removeEventListener(eventName, handler);
8639
+ return () => target.removeEventListener(eventName, handler, options);
8152
8640
  }
8153
8641
 
8154
8642
  const compareByDepth = (a, b) => a.depth - b.depth;
@@ -9418,7 +9906,6 @@
9418
9906
  */
9419
9907
  this.pendingAnimation = frame.update(() => {
9420
9908
  globalProjectionState.hasAnimatedSinceResize = true;
9421
- activeAnimations.layout++;
9422
9909
  this.motionValue || (this.motionValue = motionValue(0));
9423
9910
  this.motionValue.jump(0, false);
9424
9911
  this.currentAnimation = animateSingleValue(this.motionValue, [0, 1000], {
@@ -9429,11 +9916,7 @@
9429
9916
  this.mixTargetDelta(latest);
9430
9917
  options.onUpdate && options.onUpdate(latest);
9431
9918
  },
9432
- onStop: () => {
9433
- activeAnimations.layout--;
9434
- },
9435
9919
  onComplete: () => {
9436
- activeAnimations.layout--;
9437
9920
  options.onComplete && options.onComplete();
9438
9921
  this.completeAnimation();
9439
9922
  },
@@ -9948,7 +10431,7 @@
9948
10431
  */
9949
10432
  const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
9950
10433
  ? Math.round
9951
- : noop$1;
10434
+ : noop;
9952
10435
  function roundAxis(axis) {
9953
10436
  // Round to the nearest .5 pixels to support subpixel layouts
9954
10437
  axis.min = roundPoint(axis.min);
@@ -10022,39 +10505,221 @@
10022
10505
  checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
10023
10506
  });
10024
10507
 
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);
10508
+ const layoutSelector = "[data-layout],[data-layout-id]";
10509
+ /**
10510
+ * All imperatively-created projection nodes live in one persistent tree,
10511
+ * shared across animateLayout() calls (and with any React-created nodes,
10512
+ * via the singleton document root). Keyed by element for reuse.
10513
+ */
10514
+ const layoutNodes = new WeakMap();
10515
+ /**
10516
+ * Builders created within the same synchronous tick are flushed together
10517
+ * as a single "commit": every node is snapshotted before any updateDom
10518
+ * runs, mirroring React batching renders from different parts of the tree.
10519
+ */
10520
+ let pendingBuilders;
10521
+ function collectLayoutElements(scope) {
10522
+ const elements = [];
10523
+ if (scope instanceof HTMLElement && scope.matches(layoutSelector)) {
10524
+ elements.push(scope);
10525
+ }
10526
+ scope.querySelectorAll(layoutSelector).forEach((element) => {
10527
+ if (element instanceof HTMLElement)
10528
+ elements.push(element);
10529
+ });
10530
+ return elements;
10531
+ }
10532
+ /**
10533
+ * Process any work scheduled on the frameloop now. A previous animation
10534
+ * may have been seeked while paused (controls.time = x) without a frame
10535
+ * having rendered it - we must materialise that state into the DOM
10536
+ * before taking snapshots.
10537
+ */
10538
+ function flushPendingFrame() {
10539
+ if (frameData.isProcessing)
10540
+ return;
10541
+ const now = time.now();
10542
+ frameData.delta = clamp(0, 1000 / 60, now - frameData.timestamp);
10543
+ frameData.timestamp = now;
10544
+ frameData.isProcessing = true;
10545
+ frameSteps.update.process(frameData);
10546
+ frameSteps.preRender.process(frameData);
10547
+ frameSteps.render.process(frameData);
10548
+ frameData.isProcessing = false;
10549
+ }
10550
+ function getProjectionParent(element) {
10551
+ let ancestor = element.parentElement;
10552
+ while (ancestor) {
10553
+ const node = layoutNodes.get(ancestor);
10554
+ if (node && node.instance)
10555
+ return node;
10556
+ ancestor = ancestor.parentElement;
10557
+ }
10558
+ return undefined;
10559
+ }
10560
+ function createVisualElement() {
10561
+ return new HTMLVisualElement({
10562
+ props: {},
10563
+ presenceContext: null,
10564
+ visualState: {
10565
+ latestValues: {},
10566
+ renderState: {
10567
+ transform: {},
10568
+ transformOrigin: {},
10569
+ style: {},
10570
+ vars: {},
10571
+ },
10572
+ },
10573
+ }, { allowProjection: true });
10574
+ }
10575
+ function readNodeOptions(element, transition) {
10576
+ const layoutAttr = element.getAttribute("data-layout");
10577
+ const layoutId = element.getAttribute("data-layout-id") ?? undefined;
10035
10578
  return {
10036
- animationId: projection.root?.animationId ?? 0,
10037
- measuredBox,
10038
- layoutBox,
10039
- latestValues: projection.animationValues || projection.latestValues || {},
10040
- source: projection.id,
10579
+ layoutId,
10580
+ layout: layoutAttr !== null ? true : undefined,
10581
+ animationType: (!layoutAttr || layoutAttr === "true"
10582
+ ? "both"
10583
+ : layoutAttr),
10584
+ transition,
10041
10585
  };
10042
10586
  }
10587
+ function prepareNode(element, transition) {
10588
+ let node = layoutNodes.get(element);
10589
+ if (!node) {
10590
+ let visualElement = visualElementStore.get(element);
10591
+ if (!visualElement)
10592
+ visualElement = createVisualElement();
10593
+ /**
10594
+ * A first-time element may carry a projection transform in its
10595
+ * inline style (e.g. it was cloned from an element mid-animation).
10596
+ * That transform isn't tracked in latestValues so the engine can't
10597
+ * reset it before measuring - clear it now so the first layout
10598
+ * measurement isn't inflated.
10599
+ */
10600
+ if (element.style.transform &&
10601
+ !hasTransform(visualElement.latestValues)) {
10602
+ element.style.transform = "";
10603
+ }
10604
+ node = new HTMLProjectionNode(visualElement.latestValues, getProjectionParent(element));
10605
+ visualElement.projection = node;
10606
+ node.setOptions({
10607
+ ...readNodeOptions(element, transition),
10608
+ visualElement,
10609
+ });
10610
+ node.mount(element);
10611
+ layoutNodes.set(element, node);
10612
+ }
10613
+ else {
10614
+ node.setOptions(readNodeOptions(element, transition));
10615
+ }
10616
+ node.isPresent = true;
10617
+ if (node.options.onExitComplete) {
10618
+ node.setOptions({ onExitComplete: undefined });
10619
+ }
10620
+ return node;
10621
+ }
10622
+ function sortDocumentOrder(elements) {
10623
+ return [...elements].sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
10624
+ }
10625
+ function dropNode(element, node) {
10626
+ node.setOptions({ onExitComplete: undefined });
10627
+ /**
10628
+ * Stop any lingering animation so it can't leak into future updates.
10629
+ * A follow node can share its currentAnimation with a surviving lead
10630
+ * (via resumingFrom), in which case it isn't ours to stop.
10631
+ */
10632
+ const stack = node.getStack();
10633
+ if (!stack || node.isLead())
10634
+ node.currentAnimation?.stop();
10635
+ node.unmount();
10636
+ layoutNodes.delete(element);
10637
+ }
10638
+ function flushPendingBuilders() {
10639
+ const builders = pendingBuilders;
10640
+ pendingBuilders = undefined;
10641
+ flushPendingFrame();
10642
+ /**
10643
+ * Discover and mount every node across all builders before snapshotting
10644
+ * any of them. Mounting during an active update flags isLayoutDirty,
10645
+ * which would make that node's own willUpdate skip its snapshot.
10646
+ * Document order guarantees ancestors mount before descendants, even
10647
+ * when they're discovered by different builders.
10648
+ */
10649
+ const targets = new Map();
10650
+ for (const builder of builders) {
10651
+ for (const element of builder.collectTargets()) {
10652
+ const owners = targets.get(element);
10653
+ owners ? owners.push(builder) : targets.set(element, [builder]);
10654
+ }
10655
+ }
10656
+ const union = new Map();
10657
+ for (const element of sortDocumentOrder(targets.keys())) {
10658
+ const owners = targets.get(element);
10659
+ const node = prepareNode(element, owners[owners.length - 1].transitionFor(element));
10660
+ for (const owner of owners)
10661
+ owner.adopt(element, node);
10662
+ union.set(element, node);
10663
+ }
10664
+ union.forEach((node) => {
10665
+ node.isLayoutDirty = false;
10666
+ node.willUpdate();
10667
+ });
10668
+ const updatePromises = [];
10669
+ for (const builder of builders) {
10670
+ const result = builder.runUpdate();
10671
+ if (result)
10672
+ updatePromises.push(result);
10673
+ }
10674
+ const commit = () => {
10675
+ /**
10676
+ * Process all additions before any removals so that, even across
10677
+ * builders, a removed member knows whether a replacement with the
10678
+ * same layoutId was added in this commit.
10679
+ */
10680
+ const newMemberIds = new Set();
10681
+ for (const builder of builders) {
10682
+ builder.reconcileAdditions(newMemberIds);
10683
+ }
10684
+ for (const builder of builders) {
10685
+ builder.reconcileRemovals(newMemberIds);
10686
+ }
10687
+ let root;
10688
+ union.forEach((node) => (root || (root = node.root)));
10689
+ for (const builder of builders)
10690
+ root || (root = builder.getRoot());
10691
+ root?.didUpdate();
10692
+ /**
10693
+ * The root flushes the update on a microtask, synchronously
10694
+ * processing the frame that creates the layout animations. Collect
10695
+ * them in a later microtask step of the same pass.
10696
+ */
10697
+ microtask.render(() => {
10698
+ for (const builder of builders)
10699
+ builder.finalize();
10700
+ });
10701
+ };
10702
+ updatePromises.length ? Promise.all(updatePromises).then(commit) : commit();
10703
+ }
10043
10704
  class LayoutAnimationBuilder {
10044
10705
  constructor(scope, updateDom, defaultOptions) {
10045
- this.sharedTransitions = new Map();
10046
- this.notifyReady = noop;
10047
- this.rejectReady = noop;
10048
10706
  this.scope = scope;
10049
10707
  this.updateDom = updateDom;
10050
10708
  this.defaultOptions = defaultOptions;
10709
+ this.sharedTransitions = new Map();
10710
+ this.notifyReady = () => { };
10711
+ this.rejectReady = () => { };
10712
+ this.tracked = new Map();
10713
+ this.restorePoints = new Map();
10051
10714
  this.readyPromise = new Promise((resolve, reject) => {
10052
10715
  this.notifyReady = resolve;
10053
10716
  this.rejectReady = reject;
10054
10717
  });
10055
- frame.postRender(() => {
10056
- this.start().then(this.notifyReady).catch(this.rejectReady);
10057
- });
10718
+ if (!pendingBuilders) {
10719
+ pendingBuilders = [];
10720
+ queueMicrotask(flushPendingBuilders);
10721
+ }
10722
+ pendingBuilders.push(this);
10058
10723
  }
10059
10724
  shared(id, transition) {
10060
10725
  this.sharedTransitions.set(id, transition);
@@ -10063,114 +10728,107 @@
10063
10728
  then(resolve, reject) {
10064
10729
  return this.readyPromise.then(resolve, reject);
10065
10730
  }
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();
10731
+ transitionFor(element) {
10732
+ const layoutId = element.getAttribute("data-layout-id");
10733
+ return ((layoutId && this.sharedTransitions.get(layoutId)) ||
10734
+ this.defaultOptions);
10735
+ }
10736
+ adopt(element, node) {
10737
+ this.tracked.set(element, node);
10738
+ this.restorePoints.set(element, {
10739
+ parent: element.parentElement,
10740
+ next: element.nextSibling,
10087
10741
  });
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 = "";
10742
+ }
10743
+ collectTargets() {
10744
+ return collectLayoutElements(this.scope);
10745
+ }
10746
+ runUpdate() {
10747
+ try {
10748
+ const result = this.updateDom();
10749
+ if (result && typeof result.then === "function") {
10750
+ return result.then(undefined, (error) => {
10751
+ this.updateError = error;
10752
+ });
10107
10753
  }
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;
10754
+ }
10755
+ catch (error) {
10756
+ this.updateError = error;
10757
+ }
10758
+ return undefined;
10120
10759
  }
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);
10760
+ reconcileAdditions(newMemberIds) {
10761
+ for (const element of collectLayoutElements(this.scope)) {
10762
+ if (this.tracked.has(element))
10763
+ continue;
10764
+ const node = prepareNode(element, this.transitionFor(element));
10765
+ this.adopt(element, node);
10766
+ node.options.layoutId && newMemberIds.add(node.options.layoutId);
10139
10767
  }
10140
- return records;
10141
10768
  }
10142
- handleExitingElements(beforeRecords, afterRecords) {
10143
- const afterElementsSet = new Set(afterRecords.map((record) => record.element));
10144
- beforeRecords.forEach((record) => {
10145
- if (afterElementsSet.has(record.element))
10769
+ reconcileRemovals(newMemberIds) {
10770
+ this.tracked.forEach((node, element) => {
10771
+ if (element.isConnected)
10146
10772
  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);
10773
+ const restore = this.restorePoints.get(element);
10774
+ this.restorePoints.delete(element);
10775
+ const { layoutId } = node.options;
10776
+ const stack = node.getStack();
10777
+ const hasSurvivor = stack &&
10778
+ stack.members.some((member) => member !== node &&
10779
+ member.instance
10780
+ ?.isConnected);
10781
+ /**
10782
+ * A removed lead with a surviving stack member - and no
10783
+ * replacement member added this commit - runs an exit
10784
+ * crossfade: restore the element to its old position in the
10785
+ * DOM, relegate it and let the survivor take over. It's
10786
+ * removed again once the animation completes.
10787
+ */
10788
+ if (layoutId &&
10789
+ node.isLead() &&
10790
+ hasSurvivor &&
10791
+ !newMemberIds.has(layoutId)) {
10792
+ if (restore && restore.parent.isConnected) {
10793
+ restore.parent.insertBefore(element, restore.next && restore.next.parentNode === restore.parent
10794
+ ? restore.next
10795
+ : null);
10796
+ node.isPresent = false;
10797
+ node.setOptions({
10798
+ onExitComplete: () => {
10799
+ element.remove();
10800
+ dropNode(element, node);
10801
+ },
10802
+ });
10803
+ if (node.relegate())
10804
+ return;
10805
+ element.remove();
10806
+ }
10807
+ }
10808
+ dropNode(element, node);
10809
+ this.tracked.delete(element);
10155
10810
  });
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;
10811
+ }
10812
+ getRoot() {
10813
+ let root;
10814
+ this.tracked.forEach((node) => (root || (root = node.root)));
10815
+ return root;
10816
+ }
10817
+ finalize() {
10818
+ if (this.updateError) {
10819
+ this.rejectReady(this.updateError);
10820
+ return;
10821
+ }
10822
+ const animations = new Set();
10823
+ this.tracked.forEach((node) => {
10824
+ if (node.instance && node.currentAnimation) {
10825
+ animations.add(node.currentAnimation);
10168
10826
  }
10169
10827
  });
10828
+ this.notifyReady(new GroupAnimation([...animations]));
10170
10829
  }
10171
10830
  }
10172
10831
  function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
10173
- // animateLayout(updateDom)
10174
10832
  if (typeof scopeOrUpdateDom === "function") {
10175
10833
  return {
10176
10834
  scope: document,
@@ -10178,107 +10836,15 @@
10178
10836
  defaultOptions: updateDomOrOptions,
10179
10837
  };
10180
10838
  }
10181
- // animateLayout(scope, updateDom, options?)
10182
- const elements = resolveElements(scopeOrUpdateDom);
10183
- const scope = elements[0] || document;
10839
+ const scope = scopeOrUpdateDom instanceof Document
10840
+ ? scopeOrUpdateDom
10841
+ : resolveElements(scopeOrUpdateDom)[0] ?? document;
10184
10842
  return {
10185
10843
  scope,
10186
10844
  updateDom: updateDomOrOptions,
10187
10845
  defaultOptions: options,
10188
10846
  };
10189
10847
  }
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
10848
 
10283
10849
  /**
10284
10850
  * @deprecated
@@ -11279,7 +11845,7 @@
11279
11845
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
11280
11846
  function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
11281
11847
  if (!container)
11282
- return noop$1;
11848
+ return noop;
11283
11849
  let containerHandlers = onScrollHandlers.get(container);
11284
11850
  /**
11285
11851
  * Get the onScroll handlers for this container.
@@ -11563,7 +12129,7 @@
11563
12129
 
11564
12130
  function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
11565
12131
  if (!container)
11566
- return noop$1;
12132
+ return noop;
11567
12133
  const optionsWithDefaults = { axis, container, ...options };
11568
12134
  return typeof onScroll === "function"
11569
12135
  ? attachToFunction(onScroll, optionsWithDefaults)
@@ -11643,7 +12209,6 @@
11643
12209
  exports.ViewTransitionBuilder = ViewTransitionBuilder;
11644
12210
  exports.VisualElement = VisualElement;
11645
12211
  exports.acceleratedValues = acceleratedValues;
11646
- exports.activeAnimations = activeAnimations;
11647
12212
  exports.addAttrValue = addAttrValue;
11648
12213
  exports.addDomEvent = addDomEvent;
11649
12214
  exports.addScaleCorrector = addScaleCorrector;
@@ -11842,7 +12407,7 @@
11842
12407
  exports.motionValue = motionValue;
11843
12408
  exports.moveItem = moveItem;
11844
12409
  exports.nodeGroup = nodeGroup;
11845
- exports.noop = noop$1;
12410
+ exports.noop = noop;
11846
12411
  exports.number = number;
11847
12412
  exports.numberValueTypes = numberValueTypes;
11848
12413
  exports.observeTimeline = observeTimeline;