motion 12.27.5 → 12.28.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,7 +81,7 @@
81
81
  }
82
82
 
83
83
  /*#__NO_SIDE_EFFECTS__*/
84
- const noop = (any) => any;
84
+ const noop$1 = (any) => any;
85
85
 
86
86
  /**
87
87
  * Pipe
@@ -227,7 +227,7 @@
227
227
  function cubicBezier(mX1, mY1, mX2, mY2) {
228
228
  // If this is a linear gradient, return linear easing
229
229
  if (mX1 === mY1 && mX2 === mY2)
230
- return noop;
230
+ return noop$1;
231
231
  const getTForX = (aX) => binarySubdivide(aX, 0, 1, mX1, mX2);
232
232
  // If animation is at start/end, return t without easing
233
233
  return (t) => t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2);
@@ -278,7 +278,7 @@
278
278
  const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
279
279
 
280
280
  const easingLookup = {
281
- linear: noop,
281
+ linear: noop$1,
282
282
  easeIn,
283
283
  easeInOut,
284
284
  easeOut,
@@ -479,7 +479,7 @@
479
479
  return { schedule, cancel, state, steps };
480
480
  }
481
481
 
482
- const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
482
+ const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop$1, true);
483
483
 
484
484
  let now;
485
485
  function clearTime() {
@@ -1429,7 +1429,7 @@
1429
1429
  for (let i = 0; i < numMixers; i++) {
1430
1430
  let mixer = mixerFactory(output[i], output[i + 1]);
1431
1431
  if (ease) {
1432
- const easingFunction = Array.isArray(ease) ? ease[i] || noop : ease;
1432
+ const easingFunction = Array.isArray(ease) ? ease[i] || noop$1 : ease;
1433
1433
  mixer = pipe(easingFunction, mixer);
1434
1434
  }
1435
1435
  mixers.push(mixer);
@@ -2491,7 +2491,7 @@
2491
2491
  this.animation.onfinish = null;
2492
2492
  if (timeline && supportsScrollTimeline()) {
2493
2493
  this.animation.timeline = timeline;
2494
- return noop;
2494
+ return noop$1;
2495
2495
  }
2496
2496
  else {
2497
2497
  return observe(this);
@@ -2783,7 +2783,7 @@
2783
2783
  : new JSAnimation(resolvedOptions);
2784
2784
  animation.finished.then(() => {
2785
2785
  this.notifyFinished();
2786
- }).catch(noop);
2786
+ }).catch(noop$1);
2787
2787
  if (this.pendingTimeline) {
2788
2788
  this.stopTimeline = animation.attachTimeline(this.pendingTimeline);
2789
2789
  this.pendingTimeline = undefined;
@@ -4425,21 +4425,17 @@
4425
4425
  const styleEffect = /*@__PURE__*/ createSelectorEffect(
4426
4426
  /*@__PURE__*/ createEffect(addStyleValue));
4427
4427
 
4428
+ const toPx = px.transform;
4428
4429
  function addSVGPathValue(element, state, key, value) {
4429
4430
  frame.render(() => element.setAttribute("pathLength", "1"));
4430
4431
  if (key === "pathOffset") {
4431
- return state.set(key, value, () => {
4432
- // Use unitless value to avoid Safari zoom bug
4433
- const offset = state.latest[key];
4434
- element.setAttribute("stroke-dashoffset", `${-offset}`);
4435
- });
4432
+ return state.set(key, value, () => element.setAttribute("stroke-dashoffset", toPx(-state.latest[key])));
4436
4433
  }
4437
4434
  else {
4438
4435
  if (!state.get("stroke-dasharray")) {
4439
4436
  state.set("stroke-dasharray", new MotionValue("1 1"), () => {
4440
4437
  const { pathLength = 1, pathSpacing } = state.latest;
4441
- // Use unitless values to avoid Safari zoom bug
4442
- element.setAttribute("stroke-dasharray", `${pathLength} ${pathSpacing ?? 1 - Number(pathLength)}`);
4438
+ element.setAttribute("stroke-dasharray", `${toPx(pathLength)} ${toPx(pathSpacing ?? 1 - Number(pathLength))}`);
4443
4439
  });
4444
4440
  }
4445
4441
  return state.set(key, value, undefined, state.get("stroke-dasharray"));
@@ -5406,7 +5402,7 @@
5406
5402
  constructor(update, options = {}) {
5407
5403
  this.currentSubject = "root";
5408
5404
  this.targets = new Map();
5409
- this.notifyReady = noop;
5405
+ this.notifyReady = noop$1;
5410
5406
  this.readyPromise = new Promise((resolve) => {
5411
5407
  this.notifyReady = resolve;
5412
5408
  });
@@ -6642,9 +6638,6 @@
6642
6638
  * and stroke-dasharray attributes.
6643
6639
  *
6644
6640
  * This function is mutative to reduce per-frame GC.
6645
- *
6646
- * Note: We use unitless values for stroke-dasharray and stroke-dashoffset
6647
- * because Safari incorrectly scales px values when the page is zoomed.
6648
6641
  */
6649
6642
  function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) {
6650
6643
  // Normalise path length by setting SVG attribute pathLength to 1
@@ -6652,10 +6645,12 @@
6652
6645
  // We use dash case when setting attributes directly to the DOM node and camel case
6653
6646
  // when defining props on a React component.
6654
6647
  const keys = useDashCase ? dashKeys : camelKeys;
6655
- // Build the dash offset (unitless to avoid Safari zoom bug)
6656
- attrs[keys.offset] = `${-offset}`;
6657
- // Build the dash array (unitless to avoid Safari zoom bug)
6658
- attrs[keys.array] = `${length} ${spacing}`;
6648
+ // Build the dash offset
6649
+ attrs[keys.offset] = px.transform(-offset);
6650
+ // Build the dash array
6651
+ const pathLength = px.transform(length);
6652
+ const pathSpacing = px.transform(spacing);
6653
+ attrs[keys.array] = `${pathLength} ${pathSpacing}`;
6659
6654
  }
6660
6655
 
6661
6656
  /**
@@ -7462,7 +7457,7 @@
7462
7457
  : values.borderRadius;
7463
7458
  }
7464
7459
  const easeCrossfadeIn = /*@__PURE__*/ compress(0, 0.5, circOut);
7465
- const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop);
7460
+ const easeCrossfadeOut = /*@__PURE__*/ compress(0.5, 0.95, noop$1);
7466
7461
  function compress(min, max, easing) {
7467
7462
  return (p) => {
7468
7463
  // Could replace ifs with clamp
@@ -7688,7 +7683,7 @@
7688
7683
  cancelTreeOptimisedTransformAnimations(parent);
7689
7684
  }
7690
7685
  }
7691
- function createProjectionNode$1({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
7686
+ function createProjectionNode({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
7692
7687
  return class ProjectionNode {
7693
7688
  constructor(latestValues = {}, parent = defaultParent?.()) {
7694
7689
  /**
@@ -9226,7 +9221,7 @@
9226
9221
  */
9227
9222
  const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
9228
9223
  ? Math.round
9229
- : noop;
9224
+ : noop$1;
9230
9225
  function roundAxis(axis) {
9231
9226
  // Round to the nearest .5 pixels to support subpixel layouts
9232
9227
  axis.min = roundPoint(axis.min);
@@ -9245,11 +9240,11 @@
9245
9240
  return node !== node.root && node.scroll?.wasRoot;
9246
9241
  }
9247
9242
 
9248
- const DocumentProjectionNode = createProjectionNode$1({
9243
+ const DocumentProjectionNode = createProjectionNode({
9249
9244
  attachResizeListener: (ref, notify) => addDomEvent(ref, "resize", notify),
9250
9245
  measureScroll: () => ({
9251
- x: document.documentElement.scrollLeft || document.body?.scrollLeft || 0,
9252
- y: document.documentElement.scrollTop || document.body?.scrollTop || 0,
9246
+ x: document.documentElement.scrollLeft || document.body.scrollLeft,
9247
+ y: document.documentElement.scrollTop || document.body.scrollTop,
9253
9248
  }),
9254
9249
  checkIsScrollRoot: () => true,
9255
9250
  });
@@ -9280,7 +9275,7 @@
9280
9275
  const rootProjectionNode = {
9281
9276
  current: undefined,
9282
9277
  };
9283
- const HTMLProjectionNode = createProjectionNode$1({
9278
+ const HTMLProjectionNode = createProjectionNode({
9284
9279
  measureScroll: (instance) => ({
9285
9280
  x: instance.scrollLeft,
9286
9281
  y: instance.scrollTop,
@@ -9300,315 +9295,153 @@
9300
9295
  checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9301
9296
  });
9302
9297
 
9303
- const LAYOUT_SELECTOR = "[data-layout], [data-layout-id]";
9304
- function getLayoutElements(scope) {
9305
- const elements = Array.from(scope.querySelectorAll(LAYOUT_SELECTOR));
9306
- // Include scope itself if it's an Element (not Document) and has layout attributes
9307
- if (scope instanceof Element && hasLayout(scope)) {
9308
- elements.unshift(scope);
9309
- }
9310
- return elements;
9311
- }
9312
- function getLayoutId(element) {
9313
- return element.getAttribute("data-layout-id");
9314
- }
9315
- function hasLayout(element) {
9316
- return (element.hasAttribute("data-layout") ||
9317
- element.hasAttribute("data-layout-id"));
9318
- }
9319
-
9320
- let scaleCorrectorAdded = false;
9321
- /**
9322
- * Track active projection nodes per element to handle animation interruption.
9323
- * When a new animation starts on an element that already has an active animation,
9324
- * we need to stop the old animation so the new one can start from the current
9325
- * visual position.
9326
- */
9327
- const activeProjectionNodes = new WeakMap();
9328
- function ensureScaleCorrectors() {
9329
- if (scaleCorrectorAdded)
9330
- return;
9331
- scaleCorrectorAdded = true;
9332
- addScaleCorrector({
9333
- borderRadius: {
9334
- ...correctBorderRadius,
9335
- applyTo: [
9336
- "borderTopLeftRadius",
9337
- "borderTopRightRadius",
9338
- "borderBottomLeftRadius",
9339
- "borderBottomRightRadius",
9340
- ],
9341
- },
9342
- borderTopLeftRadius: correctBorderRadius,
9343
- borderTopRightRadius: correctBorderRadius,
9344
- borderBottomLeftRadius: correctBorderRadius,
9345
- borderBottomRightRadius: correctBorderRadius,
9346
- boxShadow: correctBoxShadow,
9347
- });
9348
- }
9349
- /**
9350
- * Get DOM depth of an element
9351
- */
9352
- function getDepth(element) {
9353
- let depth = 0;
9354
- let current = element.parentElement;
9355
- while (current) {
9356
- depth++;
9357
- current = current.parentElement;
9358
- }
9359
- return depth;
9360
- }
9361
- /**
9362
- * Find the closest projection parent for an element
9363
- */
9364
- function findProjectionParent(element, nodeCache) {
9365
- let parent = element.parentElement;
9366
- while (parent) {
9367
- const node = nodeCache.get(parent);
9368
- if (node)
9369
- return node;
9370
- parent = parent.parentElement;
9371
- }
9372
- return undefined;
9373
- }
9374
- /**
9375
- * Create or reuse a projection node for an element
9376
- */
9377
- function createProjectionNode(element, parent, options, transition) {
9378
- // Check for existing active node - reuse it to preserve animation state
9379
- const existingNode = activeProjectionNodes.get(element);
9380
- if (existingNode) {
9381
- const visualElement = existingNode.options.visualElement;
9382
- // Update transition options for the new animation
9383
- const nodeTransition = transition
9384
- ? { duration: transition.duration, ease: transition.ease }
9385
- : { duration: 0.3, ease: "easeOut" };
9386
- existingNode.setOptions({
9387
- ...existingNode.options,
9388
- animate: true,
9389
- transition: nodeTransition,
9390
- ...options,
9391
- });
9392
- // Re-mount the node if it was previously unmounted
9393
- // This re-adds it to root.nodes so didUpdate() will process it
9394
- if (!existingNode.instance) {
9395
- existingNode.mount(element);
9396
- }
9397
- return { node: existingNode, visualElement };
9398
- }
9399
- // No existing node - create a new one
9400
- const latestValues = {};
9401
- const visualElement = new HTMLVisualElement({
9402
- visualState: {
9403
- latestValues,
9404
- renderState: {
9405
- transformOrigin: {},
9406
- transform: {},
9407
- style: {},
9408
- vars: {},
9409
- },
9410
- },
9411
- presenceContext: null,
9412
- props: {},
9413
- });
9414
- const node = new HTMLProjectionNode(latestValues, parent);
9415
- // Convert AnimationOptions to transition format for the projection system
9416
- const nodeTransition = transition
9417
- ? { duration: transition.duration, ease: transition.ease }
9418
- : { duration: 0.3, ease: "easeOut" };
9419
- node.setOptions({
9420
- visualElement,
9421
- layout: true,
9422
- animate: true,
9423
- transition: nodeTransition,
9424
- ...options,
9425
- });
9426
- node.mount(element);
9427
- visualElement.projection = node;
9428
- // Track this node as the active one for this element
9429
- activeProjectionNodes.set(element, node);
9430
- return { node, visualElement };
9431
- }
9432
- /**
9433
- * Build a projection tree from a list of elements
9434
- */
9435
- function buildProjectionTree(elements, existingContext, options) {
9436
- ensureScaleCorrectors();
9437
- const nodes = existingContext?.nodes ?? new Map();
9438
- const visualElements = existingContext?.visualElements ?? new Map();
9439
- const group = existingContext?.group ?? nodeGroup();
9440
- const defaultTransition = options?.defaultTransition;
9441
- const sharedTransitions = options?.sharedTransitions;
9442
- // Sort elements by DOM depth (parents before children)
9443
- const sorted = [...elements].sort((a, b) => getDepth(a) - getDepth(b));
9444
- let root = existingContext?.root;
9445
- for (const element of sorted) {
9446
- // Skip if already has a node
9447
- if (nodes.has(element))
9448
- continue;
9449
- const parent = findProjectionParent(element, nodes);
9450
- const layoutId = getLayoutId(element);
9451
- const layoutMode = element.getAttribute("data-layout");
9452
- const nodeOptions = {
9453
- layoutId: layoutId ?? undefined,
9454
- animationType: parseLayoutMode(layoutMode),
9455
- };
9456
- // Use layoutId-specific transition if available, otherwise use default
9457
- const transition = layoutId && sharedTransitions?.get(layoutId)
9458
- ? sharedTransitions.get(layoutId)
9459
- : defaultTransition;
9460
- const { node, visualElement } = createProjectionNode(element, parent, nodeOptions, transition);
9461
- nodes.set(element, node);
9462
- visualElements.set(element, visualElement);
9463
- group.add(node);
9464
- if (!root) {
9465
- root = node.root;
9466
- }
9467
- }
9298
+ const layoutSelector = "[data-layout], [data-layout-id]";
9299
+ const noop = () => { };
9300
+ function snapshotFromTarget(projection) {
9301
+ const target = projection.targetWithTransforms || projection.target;
9302
+ if (!target)
9303
+ return undefined;
9304
+ const measuredBox = createBox();
9305
+ const layoutBox = createBox();
9306
+ copyBoxInto(measuredBox, target);
9307
+ copyBoxInto(layoutBox, target);
9468
9308
  return {
9469
- nodes,
9470
- visualElements,
9471
- group,
9472
- root: root,
9309
+ animationId: projection.root?.animationId ?? 0,
9310
+ measuredBox,
9311
+ layoutBox,
9312
+ latestValues: projection.animationValues || projection.latestValues || {},
9313
+ source: projection.id,
9473
9314
  };
9474
9315
  }
9475
- /**
9476
- * Parse the data-layout attribute value
9477
- */
9478
- function parseLayoutMode(value) {
9479
- if (value === "position")
9480
- return "position";
9481
- if (value === "size")
9482
- return "size";
9483
- if (value === "preserve-aspect")
9484
- return "preserve-aspect";
9485
- return "both";
9486
- }
9487
- /**
9488
- * Clean up projection nodes for specific elements.
9489
- * If elementsToCleanup is provided, only those elements are cleaned up.
9490
- * If not provided, all nodes are cleaned up.
9491
- *
9492
- * This allows persisting elements to keep their nodes between animations,
9493
- * matching React's behavior where nodes persist for elements that remain in the DOM.
9494
- */
9495
- function cleanupProjectionTree(context, elementsToCleanup) {
9496
- const elementsToProcess = elementsToCleanup
9497
- ? [...context.nodes.entries()].filter(([el]) => elementsToCleanup.has(el))
9498
- : [...context.nodes.entries()];
9499
- for (const [element, node] of elementsToProcess) {
9500
- context.group.remove(node);
9501
- node.unmount();
9502
- // Only clear from activeProjectionNodes if this is still the active node.
9503
- // A newer animation might have already taken over.
9504
- if (activeProjectionNodes.get(element) === node) {
9505
- activeProjectionNodes.delete(element);
9506
- }
9507
- context.nodes.delete(element);
9508
- context.visualElements.delete(element);
9509
- }
9510
- }
9511
-
9512
9316
  class LayoutAnimationBuilder {
9513
9317
  constructor(scope, updateDom, defaultOptions) {
9514
9318
  this.sharedTransitions = new Map();
9515
9319
  this.notifyReady = noop;
9516
- this.executed = false;
9320
+ this.rejectReady = noop;
9517
9321
  this.scope = scope;
9518
9322
  this.updateDom = updateDom;
9519
9323
  this.defaultOptions = defaultOptions;
9520
- this.readyPromise = new Promise((resolve) => {
9324
+ this.readyPromise = new Promise((resolve, reject) => {
9521
9325
  this.notifyReady = resolve;
9326
+ this.rejectReady = reject;
9327
+ });
9328
+ frame.postRender(() => {
9329
+ this.start().then(this.notifyReady).catch(this.rejectReady);
9522
9330
  });
9523
- // Queue execution on microtask to allow builder methods to be called
9524
- queueMicrotask(() => this.execute());
9525
9331
  }
9526
- shared(id, options) {
9527
- this.sharedTransitions.set(id, options);
9332
+ shared(id, transition) {
9333
+ this.sharedTransitions.set(id, transition);
9528
9334
  return this;
9529
9335
  }
9530
- then(onfulfilled, onrejected) {
9531
- return this.readyPromise.then(onfulfilled, onrejected);
9336
+ then(resolve, reject) {
9337
+ return this.readyPromise.then(resolve, reject);
9532
9338
  }
9533
- async execute() {
9534
- if (this.executed)
9535
- return;
9536
- this.executed = true;
9537
- let context;
9538
- // Phase 1: Pre-mutation - Build projection tree and take snapshots
9539
- const beforeElements = getLayoutElements(this.scope);
9540
- if (beforeElements.length > 0) {
9541
- context = buildProjectionTree(beforeElements, undefined, this.getBuildOptions());
9542
- context.root.startUpdate();
9543
- for (const node of context.nodes.values()) {
9544
- node.isLayoutDirty = false;
9545
- node.willUpdate();
9546
- }
9547
- }
9548
- // Phase 2: Execute DOM update
9549
- this.updateDom();
9550
- // Phase 3: Post-mutation - Compare before/after elements
9551
- const afterElements = getLayoutElements(this.scope);
9552
- const beforeSet = new Set(beforeElements);
9553
- const afterSet = new Set(afterElements);
9554
- const entering = afterElements.filter((el) => !beforeSet.has(el));
9555
- const exiting = beforeElements.filter((el) => !afterSet.has(el));
9556
- // Build projection nodes for entering elements
9557
- if (entering.length > 0) {
9558
- context = buildProjectionTree(entering, context, this.getBuildOptions());
9559
- }
9560
- // No layout elements - return empty animation
9561
- if (!context) {
9562
- this.notifyReady(new GroupAnimation([]));
9563
- return;
9564
- }
9565
- // Handle shared elements
9566
- for (const element of exiting) {
9567
- const node = context.nodes.get(element);
9568
- node?.getStack()?.remove(node);
9569
- }
9570
- for (const element of entering) {
9571
- context.nodes.get(element)?.promote();
9572
- }
9573
- // Phase 4: Animate
9574
- context.root.didUpdate();
9575
- await new Promise((resolve) => frame.postRender(() => resolve()));
9576
- const animations = [];
9577
- for (const node of context.nodes.values()) {
9578
- if (node.currentAnimation) {
9579
- animations.push(node.currentAnimation);
9580
- }
9581
- }
9582
- const groupAnimation = new GroupAnimation(animations);
9583
- groupAnimation.finished.then(() => {
9584
- // Only clean up nodes for elements no longer in the document.
9585
- // Elements still in DOM keep their nodes so subsequent animations
9586
- // can use the stored position snapshots (A→B→A pattern).
9587
- const elementsToCleanup = new Set();
9588
- for (const element of context.nodes.keys()) {
9589
- if (!document.contains(element)) {
9590
- elementsToCleanup.add(element);
9339
+ async start() {
9340
+ const beforeElements = collectLayoutElements(this.scope);
9341
+ const beforeRecords = this.buildRecords(beforeElements);
9342
+ beforeRecords.forEach(({ projection }) => {
9343
+ const hasCurrentAnimation = Boolean(projection.currentAnimation);
9344
+ const isSharedLayout = Boolean(projection.options.layoutId);
9345
+ if (hasCurrentAnimation && isSharedLayout) {
9346
+ const snapshot = snapshotFromTarget(projection);
9347
+ if (snapshot) {
9348
+ projection.snapshot = snapshot;
9591
9349
  }
9350
+ else if (projection.snapshot) {
9351
+ projection.snapshot = undefined;
9352
+ }
9353
+ }
9354
+ else if (projection.snapshot &&
9355
+ (projection.currentAnimation || projection.isProjecting())) {
9356
+ projection.snapshot = undefined;
9357
+ }
9358
+ projection.isPresent = true;
9359
+ projection.willUpdate();
9360
+ });
9361
+ await this.updateDom();
9362
+ const afterElements = collectLayoutElements(this.scope);
9363
+ const afterRecords = this.buildRecords(afterElements);
9364
+ this.handleExitingElements(beforeRecords, afterRecords);
9365
+ afterRecords.forEach(({ projection }) => {
9366
+ const instance = projection.instance;
9367
+ const resumeFromInstance = projection.resumeFrom
9368
+ ?.instance;
9369
+ if (!instance || !resumeFromInstance)
9370
+ return;
9371
+ if (!("style" in instance))
9372
+ return;
9373
+ const currentTransform = instance.style.transform;
9374
+ const resumeFromTransform = resumeFromInstance.style.transform;
9375
+ if (currentTransform &&
9376
+ resumeFromTransform &&
9377
+ currentTransform === resumeFromTransform) {
9378
+ instance.style.transform = "";
9379
+ instance.style.transformOrigin = "";
9592
9380
  }
9593
- cleanupProjectionTree(context, elementsToCleanup);
9594
9381
  });
9595
- this.notifyReady(groupAnimation);
9382
+ afterRecords.forEach(({ projection }) => {
9383
+ projection.isPresent = true;
9384
+ });
9385
+ const root = getProjectionRoot(afterRecords, beforeRecords);
9386
+ root?.didUpdate();
9387
+ await new Promise((resolve) => {
9388
+ frame.postRender(() => resolve());
9389
+ });
9390
+ const animations = collectAnimations(afterRecords);
9391
+ const animation = new GroupAnimation(animations);
9392
+ return animation;
9596
9393
  }
9597
- getBuildOptions() {
9598
- return {
9599
- defaultTransition: this.defaultOptions || {
9600
- duration: 0.3,
9601
- ease: "easeOut",
9602
- },
9603
- sharedTransitions: this.sharedTransitions.size > 0
9604
- ? this.sharedTransitions
9605
- : undefined,
9606
- };
9394
+ buildRecords(elements) {
9395
+ const records = [];
9396
+ const recordMap = new Map();
9397
+ for (const element of elements) {
9398
+ const parentRecord = findParentRecord(element, recordMap, this.scope);
9399
+ const { layout, layoutId } = readLayoutAttributes(element);
9400
+ const override = layoutId
9401
+ ? this.sharedTransitions.get(layoutId)
9402
+ : undefined;
9403
+ const transition = override || this.defaultOptions;
9404
+ const record = getOrCreateRecord(element, parentRecord?.projection, {
9405
+ layout,
9406
+ layoutId,
9407
+ animationType: typeof layout === "string" ? layout : "both",
9408
+ transition: transition,
9409
+ });
9410
+ recordMap.set(element, record);
9411
+ records.push(record);
9412
+ }
9413
+ return records;
9414
+ }
9415
+ handleExitingElements(beforeRecords, afterRecords) {
9416
+ const afterElementsSet = new Set(afterRecords.map((record) => record.element));
9417
+ beforeRecords.forEach((record) => {
9418
+ if (afterElementsSet.has(record.element))
9419
+ return;
9420
+ // For shared layout elements, relegate to set up resumeFrom
9421
+ // so the remaining element animates from this position
9422
+ if (record.projection.options.layoutId) {
9423
+ record.projection.isPresent = false;
9424
+ record.projection.relegate();
9425
+ }
9426
+ record.visualElement.unmount();
9427
+ visualElementStore.delete(record.element);
9428
+ });
9429
+ // Clear resumeFrom on EXISTING nodes that point to unmounted projections
9430
+ // This prevents crossfade animation when the source element was removed entirely
9431
+ // But preserve resumeFrom for NEW nodes so they can animate from the old position
9432
+ // Also preserve resumeFrom for lead nodes that were just promoted via relegate
9433
+ const beforeElementsSet = new Set(beforeRecords.map((record) => record.element));
9434
+ afterRecords.forEach(({ element, projection }) => {
9435
+ if (beforeElementsSet.has(element) &&
9436
+ projection.resumeFrom &&
9437
+ !projection.resumeFrom.instance &&
9438
+ !projection.isLead()) {
9439
+ projection.resumeFrom = undefined;
9440
+ projection.snapshot = undefined;
9441
+ }
9442
+ });
9607
9443
  }
9608
9444
  }
9609
- /**
9610
- * Parse arguments for animateLayout overloads
9611
- */
9612
9445
  function parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options) {
9613
9446
  // animateLayout(updateDom)
9614
9447
  if (typeof scopeOrUpdateDom === "function") {
@@ -9622,11 +9455,98 @@
9622
9455
  const elements = resolveElements(scopeOrUpdateDom);
9623
9456
  const scope = elements[0] || document;
9624
9457
  return {
9625
- scope: scope instanceof Document ? scope : scope,
9458
+ scope,
9626
9459
  updateDom: updateDomOrOptions,
9627
9460
  defaultOptions: options,
9628
9461
  };
9629
9462
  }
9463
+ function collectLayoutElements(scope) {
9464
+ const elements = Array.from(scope.querySelectorAll(layoutSelector));
9465
+ if (scope instanceof Element && scope.matches(layoutSelector)) {
9466
+ if (!elements.includes(scope)) {
9467
+ elements.unshift(scope);
9468
+ }
9469
+ }
9470
+ return elements;
9471
+ }
9472
+ function readLayoutAttributes(element) {
9473
+ const layoutId = element.getAttribute("data-layout-id") || undefined;
9474
+ const rawLayout = element.getAttribute("data-layout");
9475
+ let layout;
9476
+ if (rawLayout === "" || rawLayout === "true") {
9477
+ layout = true;
9478
+ }
9479
+ else if (rawLayout) {
9480
+ layout = rawLayout;
9481
+ }
9482
+ return {
9483
+ layout,
9484
+ layoutId,
9485
+ };
9486
+ }
9487
+ function createVisualState() {
9488
+ return {
9489
+ latestValues: {},
9490
+ renderState: {
9491
+ transform: {},
9492
+ transformOrigin: {},
9493
+ style: {},
9494
+ vars: {},
9495
+ },
9496
+ };
9497
+ }
9498
+ function getOrCreateRecord(element, parentProjection, projectionOptions) {
9499
+ const existing = visualElementStore.get(element);
9500
+ const visualElement = existing ??
9501
+ new HTMLVisualElement({
9502
+ props: {},
9503
+ presenceContext: null,
9504
+ visualState: createVisualState(),
9505
+ }, { allowProjection: true });
9506
+ if (!existing || !visualElement.projection) {
9507
+ visualElement.projection = new HTMLProjectionNode(visualElement.latestValues, parentProjection);
9508
+ }
9509
+ visualElement.projection.setOptions({
9510
+ ...projectionOptions,
9511
+ visualElement,
9512
+ });
9513
+ if (!visualElement.current) {
9514
+ visualElement.mount(element);
9515
+ }
9516
+ if (!existing) {
9517
+ visualElementStore.set(element, visualElement);
9518
+ }
9519
+ return {
9520
+ element,
9521
+ visualElement,
9522
+ projection: visualElement.projection,
9523
+ };
9524
+ }
9525
+ function findParentRecord(element, recordMap, scope) {
9526
+ let parent = element.parentElement;
9527
+ while (parent) {
9528
+ const record = recordMap.get(parent);
9529
+ if (record)
9530
+ return record;
9531
+ if (parent === scope)
9532
+ break;
9533
+ parent = parent.parentElement;
9534
+ }
9535
+ return undefined;
9536
+ }
9537
+ function getProjectionRoot(afterRecords, beforeRecords) {
9538
+ const record = afterRecords[0] || beforeRecords[0];
9539
+ return record?.projection.root;
9540
+ }
9541
+ function collectAnimations(afterRecords) {
9542
+ const animations = new Set();
9543
+ afterRecords.forEach((record) => {
9544
+ const animation = record.projection.currentAnimation;
9545
+ if (animation)
9546
+ animations.add(animation);
9547
+ });
9548
+ return Array.from(animations);
9549
+ }
9630
9550
 
9631
9551
  /**
9632
9552
  * @deprecated
@@ -10520,7 +10440,7 @@
10520
10440
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
10521
10441
  function scrollInfo(onScroll, { container = document.scrollingElement, ...options } = {}) {
10522
10442
  if (!container)
10523
- return noop;
10443
+ return noop$1;
10524
10444
  let containerHandlers = onScrollHandlers.get(container);
10525
10445
  /**
10526
10446
  * Get the onScroll handlers for this container.
@@ -10648,7 +10568,7 @@
10648
10568
 
10649
10569
  function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
10650
10570
  if (!container)
10651
- return noop;
10571
+ return noop$1;
10652
10572
  const optionsWithDefaults = { axis, container, ...options };
10653
10573
  return typeof onScroll === "function"
10654
10574
  ? attachToFunction(onScroll, optionsWithDefaults)
@@ -10809,7 +10729,7 @@
10809
10729
  exports.createBox = createBox;
10810
10730
  exports.createDelta = createDelta;
10811
10731
  exports.createGeneratorEasing = createGeneratorEasing;
10812
- exports.createProjectionNode = createProjectionNode$1;
10732
+ exports.createProjectionNode = createProjectionNode;
10813
10733
  exports.createRenderBatcher = createRenderBatcher;
10814
10734
  exports.createScopedAnimate = createScopedAnimate;
10815
10735
  exports.cubicBezier = cubicBezier;
@@ -10923,7 +10843,7 @@
10923
10843
  exports.motionValue = motionValue;
10924
10844
  exports.moveItem = moveItem;
10925
10845
  exports.nodeGroup = nodeGroup;
10926
- exports.noop = noop;
10846
+ exports.noop = noop$1;
10927
10847
  exports.number = number;
10928
10848
  exports.numberValueTypes = numberValueTypes;
10929
10849
  exports.observeTimeline = observeTimeline;