motion 12.27.0-alpha.5 → 12.27.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.
@@ -2963,6 +2963,20 @@
2963
2963
  return map;
2964
2964
  }
2965
2965
 
2966
+ function calcChildStagger(children, child, delayChildren, staggerChildren = 0, staggerDirection = 1) {
2967
+ const index = Array.from(children)
2968
+ .sort((a, b) => a.sortNodePosition(b))
2969
+ .indexOf(child);
2970
+ const numChildren = children.size;
2971
+ const maxStaggerDuration = (numChildren - 1) * staggerChildren;
2972
+ const delayIsFunction = typeof delayChildren === "function";
2973
+ return delayIsFunction
2974
+ ? delayChildren(index, numChildren)
2975
+ : staggerDirection === 1
2976
+ ? index * staggerChildren
2977
+ : maxStaggerDuration - index * staggerChildren;
2978
+ }
2979
+
2966
2980
  /**
2967
2981
  * Parse Framer's special CSS variable format into a CSS token and a fallback.
2968
2982
  *
@@ -3000,12 +3014,6 @@
3000
3014
  : fallback;
3001
3015
  }
3002
3016
 
3003
- function getValueTransition$1(transition, key) {
3004
- return (transition?.[key] ??
3005
- transition?.["default"] ??
3006
- transition);
3007
- }
3008
-
3009
3017
  const underDampedSpring = {
3010
3018
  type: "spring",
3011
3019
  stiffness: 500,
@@ -3043,15 +3051,6 @@
3043
3051
  return ease;
3044
3052
  };
3045
3053
 
3046
- /**
3047
- * Decide whether a transition is defined on a given Transition.
3048
- * This filters out orchestration options and returns true
3049
- * if any options are left.
3050
- */
3051
- function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
3052
- return !!Object.keys(transition).length;
3053
- }
3054
-
3055
3054
  const isNotNull = (value) => value !== null;
3056
3055
  function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
3057
3056
  const resolvedKeyframes = keyframes.filter(isNotNull);
@@ -3063,18 +3062,19 @@
3063
3062
  : finalKeyframe;
3064
3063
  }
3065
3064
 
3066
- function calcChildStagger(children, child, delayChildren, staggerChildren = 0, staggerDirection = 1) {
3067
- const index = Array.from(children)
3068
- .sort((a, b) => a.sortNodePosition(b))
3069
- .indexOf(child);
3070
- const numChildren = children.size;
3071
- const maxStaggerDuration = (numChildren - 1) * staggerChildren;
3072
- const delayIsFunction = typeof delayChildren === "function";
3073
- return delayIsFunction
3074
- ? delayChildren(index, numChildren)
3075
- : staggerDirection === 1
3076
- ? index * staggerChildren
3077
- : maxStaggerDuration - index * staggerChildren;
3065
+ function getValueTransition$1(transition, key) {
3066
+ return (transition?.[key] ??
3067
+ transition?.["default"] ??
3068
+ transition);
3069
+ }
3070
+
3071
+ /**
3072
+ * Decide whether a transition is defined on a given Transition.
3073
+ * This filters out orchestration options and returns true
3074
+ * if any options are left.
3075
+ */
3076
+ function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
3077
+ return !!Object.keys(transition).length;
3078
3078
  }
3079
3079
 
3080
3080
  const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
@@ -3168,6 +3168,46 @@
3168
3168
  : new AsyncMotionValueAnimation(options);
3169
3169
  };
3170
3170
 
3171
+ function getValueState(visualElement) {
3172
+ const state = [{}, {}];
3173
+ visualElement?.values.forEach((value, key) => {
3174
+ state[0][key] = value.get();
3175
+ state[1][key] = value.getVelocity();
3176
+ });
3177
+ return state;
3178
+ }
3179
+ function resolveVariantFromProps(props, definition, custom, visualElement) {
3180
+ /**
3181
+ * If the variant definition is a function, resolve.
3182
+ */
3183
+ if (typeof definition === "function") {
3184
+ const [current, velocity] = getValueState(visualElement);
3185
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3186
+ }
3187
+ /**
3188
+ * If the variant definition is a variant label, or
3189
+ * the function returned a variant label, resolve.
3190
+ */
3191
+ if (typeof definition === "string") {
3192
+ definition = props.variants && props.variants[definition];
3193
+ }
3194
+ /**
3195
+ * At this point we've resolved both functions and variant labels,
3196
+ * but the resolved variant label might itself have been a function.
3197
+ * If so, resolve. This can only have returned a valid target object.
3198
+ */
3199
+ if (typeof definition === "function") {
3200
+ const [current, velocity] = getValueState(visualElement);
3201
+ definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3202
+ }
3203
+ return definition;
3204
+ }
3205
+
3206
+ function resolveVariant(visualElement, definition, custom) {
3207
+ const props = visualElement.getProps();
3208
+ return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3209
+ }
3210
+
3171
3211
  const positionalKeys = new Set([
3172
3212
  "width",
3173
3213
  "height",
@@ -3496,46 +3536,6 @@
3496
3536
  return new MotionValue(init, options);
3497
3537
  }
3498
3538
 
3499
- function getValueState(visualElement) {
3500
- const state = [{}, {}];
3501
- visualElement?.values.forEach((value, key) => {
3502
- state[0][key] = value.get();
3503
- state[1][key] = value.getVelocity();
3504
- });
3505
- return state;
3506
- }
3507
- function resolveVariantFromProps(props, definition, custom, visualElement) {
3508
- /**
3509
- * If the variant definition is a function, resolve.
3510
- */
3511
- if (typeof definition === "function") {
3512
- const [current, velocity] = getValueState(visualElement);
3513
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3514
- }
3515
- /**
3516
- * If the variant definition is a variant label, or
3517
- * the function returned a variant label, resolve.
3518
- */
3519
- if (typeof definition === "string") {
3520
- definition = props.variants && props.variants[definition];
3521
- }
3522
- /**
3523
- * At this point we've resolved both functions and variant labels,
3524
- * but the resolved variant label might itself have been a function.
3525
- * If so, resolve. This can only have returned a valid target object.
3526
- */
3527
- if (typeof definition === "function") {
3528
- const [current, velocity] = getValueState(visualElement);
3529
- definition = definition(custom !== undefined ? custom : props.custom, current, velocity);
3530
- }
3531
- return definition;
3532
- }
3533
-
3534
- function resolveVariant(visualElement, definition, custom) {
3535
- const props = visualElement.getProps();
3536
- return resolveVariantFromProps(props, definition, custom !== undefined ? custom : props.custom, visualElement);
3537
- }
3538
-
3539
3539
  const isKeyframesTarget = (v) => {
3540
3540
  return Array.isArray(v);
3541
3541
  };
@@ -6066,19 +6066,6 @@
6066
6066
  }
6067
6067
  }
6068
6068
 
6069
- /**
6070
- * Feature base class for extending VisualElement functionality.
6071
- * Features are plugins that can be mounted/unmounted to add behavior
6072
- * like gestures, animations, or layout tracking.
6073
- */
6074
- class Feature {
6075
- constructor(node) {
6076
- this.isMounted = false;
6077
- this.node = node;
6078
- }
6079
- update() { }
6080
- }
6081
-
6082
6069
  class DOMVisualElement extends VisualElement {
6083
6070
  constructor() {
6084
6071
  super(...arguments);
@@ -6116,6 +6103,19 @@
6116
6103
  }
6117
6104
  }
6118
6105
 
6106
+ /**
6107
+ * Feature base class for extending VisualElement functionality.
6108
+ * Features are plugins that can be mounted/unmounted to add behavior
6109
+ * like gestures, animations, or layout tracking.
6110
+ */
6111
+ class Feature {
6112
+ constructor(node) {
6113
+ this.isMounted = false;
6114
+ this.node = node;
6115
+ }
6116
+ update() { }
6117
+ }
6118
+
6119
6119
  /**
6120
6120
  * Bounding boxes tend to be defined as top, left, right, bottom. For various operations
6121
6121
  * it's easier to consider each axis individually. This function returns a bounding box
@@ -6584,6 +6584,43 @@
6584
6584
  }
6585
6585
  }
6586
6586
 
6587
+ function isObjectKey(key, object) {
6588
+ return key in object;
6589
+ }
6590
+ class ObjectVisualElement extends VisualElement {
6591
+ constructor() {
6592
+ super(...arguments);
6593
+ this.type = "object";
6594
+ }
6595
+ readValueFromInstance(instance, key) {
6596
+ if (isObjectKey(key, instance)) {
6597
+ const value = instance[key];
6598
+ if (typeof value === "string" || typeof value === "number") {
6599
+ return value;
6600
+ }
6601
+ }
6602
+ return undefined;
6603
+ }
6604
+ getBaseTargetFromProps() {
6605
+ return undefined;
6606
+ }
6607
+ removeValueFromRenderState(key, renderState) {
6608
+ delete renderState.output[key];
6609
+ }
6610
+ measureInstanceViewportBox() {
6611
+ return createBox();
6612
+ }
6613
+ build(renderState, latestValues) {
6614
+ Object.assign(renderState.output, latestValues);
6615
+ }
6616
+ renderInstance(instance, { output }) {
6617
+ Object.assign(instance, output);
6618
+ }
6619
+ sortInstanceNodePosition() {
6620
+ return 0;
6621
+ }
6622
+ }
6623
+
6587
6624
  const dashKeys = {
6588
6625
  offset: "stroke-dashoffset",
6589
6626
  array: "stroke-dasharray",
@@ -6766,43 +6803,6 @@
6766
6803
  }
6767
6804
  }
6768
6805
 
6769
- function isObjectKey(key, object) {
6770
- return key in object;
6771
- }
6772
- class ObjectVisualElement extends VisualElement {
6773
- constructor() {
6774
- super(...arguments);
6775
- this.type = "object";
6776
- }
6777
- readValueFromInstance(instance, key) {
6778
- if (isObjectKey(key, instance)) {
6779
- const value = instance[key];
6780
- if (typeof value === "string" || typeof value === "number") {
6781
- return value;
6782
- }
6783
- }
6784
- return undefined;
6785
- }
6786
- getBaseTargetFromProps() {
6787
- return undefined;
6788
- }
6789
- removeValueFromRenderState(key, renderState) {
6790
- delete renderState.output[key];
6791
- }
6792
- measureInstanceViewportBox() {
6793
- return createBox();
6794
- }
6795
- build(renderState, latestValues) {
6796
- Object.assign(renderState.output, latestValues);
6797
- }
6798
- renderInstance(instance, { output }) {
6799
- Object.assign(instance, output);
6800
- }
6801
- sortInstanceNodePosition() {
6802
- return 0;
6803
- }
6804
- }
6805
-
6806
6806
  const numVariantProps = variantProps.length;
6807
6807
  /**
6808
6808
  * Get variant context from a visual element's parent chain.
@@ -7197,6 +7197,36 @@
7197
7197
  };
7198
7198
  }
7199
7199
 
7200
+ /**
7201
+ * Reset an axis to the provided origin box.
7202
+ *
7203
+ * This is a mutative operation.
7204
+ */
7205
+ function copyAxisInto(axis, originAxis) {
7206
+ axis.min = originAxis.min;
7207
+ axis.max = originAxis.max;
7208
+ }
7209
+ /**
7210
+ * Reset a box to the provided origin box.
7211
+ *
7212
+ * This is a mutative operation.
7213
+ */
7214
+ function copyBoxInto(box, originBox) {
7215
+ copyAxisInto(box.x, originBox.x);
7216
+ copyAxisInto(box.y, originBox.y);
7217
+ }
7218
+ /**
7219
+ * Reset a delta to the provided origin box.
7220
+ *
7221
+ * This is a mutative operation.
7222
+ */
7223
+ function copyAxisDeltaInto(delta, originDelta) {
7224
+ delta.translate = originDelta.translate;
7225
+ delta.scale = originDelta.scale;
7226
+ delta.originPoint = originDelta.originPoint;
7227
+ delta.origin = originDelta.origin;
7228
+ }
7229
+
7200
7230
  const SCALE_PRECISION = 0.0001;
7201
7231
  const SCALE_MIN = 1 - SCALE_PRECISION;
7202
7232
  const SCALE_MAX = 1 + SCALE_PRECISION;
@@ -7295,36 +7325,6 @@
7295
7325
  removeAxisTransforms(box.y, transforms, yKeys, originBox ? originBox.y : undefined, sourceBox ? sourceBox.y : undefined);
7296
7326
  }
7297
7327
 
7298
- /**
7299
- * Reset an axis to the provided origin box.
7300
- *
7301
- * This is a mutative operation.
7302
- */
7303
- function copyAxisInto(axis, originAxis) {
7304
- axis.min = originAxis.min;
7305
- axis.max = originAxis.max;
7306
- }
7307
- /**
7308
- * Reset a box to the provided origin box.
7309
- *
7310
- * This is a mutative operation.
7311
- */
7312
- function copyBoxInto(box, originBox) {
7313
- copyAxisInto(box.x, originBox.x);
7314
- copyAxisInto(box.y, originBox.y);
7315
- }
7316
- /**
7317
- * Reset a delta to the provided origin box.
7318
- *
7319
- * This is a mutative operation.
7320
- */
7321
- function copyAxisDeltaInto(delta, originDelta) {
7322
- delta.translate = originDelta.translate;
7323
- delta.scale = originDelta.scale;
7324
- delta.originPoint = originDelta.originPoint;
7325
- delta.origin = originDelta.origin;
7326
- }
7327
-
7328
7328
  function isAxisDeltaZero(delta) {
7329
7329
  return delta.translate === 0 && delta.scale === 1;
7330
7330
  }
@@ -7466,23 +7466,10 @@
7466
7466
  };
7467
7467
  }
7468
7468
 
7469
- /**
7470
- * Timeout defined in ms
7471
- */
7472
- function delay(callback, timeout) {
7473
- const start = time.now();
7474
- const checkElapsed = ({ timestamp }) => {
7475
- const elapsed = timestamp - start;
7476
- if (elapsed >= timeout) {
7477
- cancelFrame(checkElapsed);
7478
- callback(elapsed - timeout);
7479
- }
7480
- };
7481
- frame.setup(checkElapsed, true);
7482
- return () => cancelFrame(checkElapsed);
7483
- }
7484
- function delayInSeconds(callback, timeout) {
7485
- return delay(callback, secondsToMilliseconds(timeout));
7469
+ function animateSingleValue(value, keyframes, options) {
7470
+ const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
7471
+ motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
7472
+ return motionValue$1.animation;
7486
7473
  }
7487
7474
 
7488
7475
  function addDomEvent(target, eventName, handler, options = { passive: true }) {
@@ -7490,19 +7477,6 @@
7490
7477
  return () => target.removeEventListener(eventName, handler);
7491
7478
  }
7492
7479
 
7493
- /**
7494
- * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
7495
- */
7496
- function resolveMotionValue(value) {
7497
- return isMotionValue(value) ? value.get() : value;
7498
- }
7499
-
7500
- function animateSingleValue(value, keyframes, options) {
7501
- const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
7502
- motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
7503
- return motionValue$1.animation;
7504
- }
7505
-
7506
7480
  const compareByDepth = (a, b) => a.depth - b.depth;
7507
7481
 
7508
7482
  class FlatTree {
@@ -7525,6 +7499,32 @@
7525
7499
  }
7526
7500
  }
7527
7501
 
7502
+ /**
7503
+ * Timeout defined in ms
7504
+ */
7505
+ function delay(callback, timeout) {
7506
+ const start = time.now();
7507
+ const checkElapsed = ({ timestamp }) => {
7508
+ const elapsed = timestamp - start;
7509
+ if (elapsed >= timeout) {
7510
+ cancelFrame(checkElapsed);
7511
+ callback(elapsed - timeout);
7512
+ }
7513
+ };
7514
+ frame.setup(checkElapsed, true);
7515
+ return () => cancelFrame(checkElapsed);
7516
+ }
7517
+ function delayInSeconds(callback, timeout) {
7518
+ return delay(callback, secondsToMilliseconds(timeout));
7519
+ }
7520
+
7521
+ /**
7522
+ * If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
7523
+ */
7524
+ function resolveMotionValue(value) {
7525
+ return isMotionValue(value) ? value.get() : value;
7526
+ }
7527
+
7528
7528
  class NodeStack {
7529
7529
  constructor() {
7530
7530
  this.members = [];
@@ -9246,29 +9246,6 @@
9246
9246
  checkIsScrollRoot: () => true,
9247
9247
  });
9248
9248
 
9249
- const rootProjectionNode = {
9250
- current: undefined,
9251
- };
9252
- const HTMLProjectionNode = createProjectionNode$1({
9253
- measureScroll: (instance) => ({
9254
- x: instance.scrollLeft,
9255
- y: instance.scrollTop,
9256
- }),
9257
- defaultParent: () => {
9258
- if (!rootProjectionNode.current) {
9259
- const documentNode = new DocumentProjectionNode({});
9260
- documentNode.mount(window);
9261
- documentNode.setOptions({ layoutScroll: true });
9262
- rootProjectionNode.current = documentNode;
9263
- }
9264
- return rootProjectionNode.current;
9265
- },
9266
- resetTransform: (instance, value) => {
9267
- instance.style.transform = value !== undefined ? value : "none";
9268
- },
9269
- checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9270
- });
9271
-
9272
9249
  const notify = (node) => !node.isLayoutDirty && node.willUpdate(false);
9273
9250
  function nodeGroup() {
9274
9251
  const nodes = new Set();
@@ -9292,6 +9269,29 @@
9292
9269
  };
9293
9270
  }
9294
9271
 
9272
+ const rootProjectionNode = {
9273
+ current: undefined,
9274
+ };
9275
+ const HTMLProjectionNode = createProjectionNode$1({
9276
+ measureScroll: (instance) => ({
9277
+ x: instance.scrollLeft,
9278
+ y: instance.scrollTop,
9279
+ }),
9280
+ defaultParent: () => {
9281
+ if (!rootProjectionNode.current) {
9282
+ const documentNode = new DocumentProjectionNode({});
9283
+ documentNode.mount(window);
9284
+ documentNode.setOptions({ layoutScroll: true });
9285
+ rootProjectionNode.current = documentNode;
9286
+ }
9287
+ return rootProjectionNode.current;
9288
+ },
9289
+ resetTransform: (instance, value) => {
9290
+ instance.style.transform = value !== undefined ? value : "none";
9291
+ },
9292
+ checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
9293
+ });
9294
+
9295
9295
  const LAYOUT_SELECTOR = "[data-layout], [data-layout-id]";
9296
9296
  function getLayoutElements(scope) {
9297
9297
  const elements = Array.from(scope.querySelectorAll(LAYOUT_SELECTOR));
@@ -9308,93 +9308,6 @@
9308
9308
  return (element.hasAttribute("data-layout") ||
9309
9309
  element.hasAttribute("data-layout-id"));
9310
9310
  }
9311
- /**
9312
- * Track layout elements before mutation.
9313
- * Does NOT measure bounds - that's handled by the projection system via willUpdate().
9314
- */
9315
- function trackLayoutElements(scope) {
9316
- const elements = getLayoutElements(scope);
9317
- const records = new Map();
9318
- for (const element of elements) {
9319
- records.set(element, {
9320
- element,
9321
- parentElement: element.parentElement,
9322
- nextSibling: element.nextSibling,
9323
- layoutId: getLayoutId(element),
9324
- });
9325
- }
9326
- return records;
9327
- }
9328
- /**
9329
- * Compare before/after records to detect entering/exiting/persisting elements
9330
- */
9331
- function detectMutations(beforeRecords, scope) {
9332
- const afterElements = new Set(getLayoutElements(scope));
9333
- const beforeElements = new Set(beforeRecords.keys());
9334
- const entering = [];
9335
- const exiting = [];
9336
- const persisting = [];
9337
- const sharedEntering = new Map();
9338
- const sharedExiting = new Map();
9339
- // Find exiting elements (were in before, not in after)
9340
- for (const element of beforeElements) {
9341
- if (!afterElements.has(element)) {
9342
- const record = beforeRecords.get(element);
9343
- exiting.push({
9344
- element,
9345
- parentElement: record.parentElement,
9346
- nextSibling: record.nextSibling,
9347
- });
9348
- if (record.layoutId) {
9349
- sharedExiting.set(record.layoutId, element);
9350
- }
9351
- }
9352
- }
9353
- // Find entering and persisting elements
9354
- for (const element of afterElements) {
9355
- if (!beforeElements.has(element)) {
9356
- entering.push(element);
9357
- const layoutId = getLayoutId(element);
9358
- if (layoutId) {
9359
- sharedEntering.set(layoutId, element);
9360
- }
9361
- }
9362
- else {
9363
- persisting.push(element);
9364
- }
9365
- }
9366
- return {
9367
- entering,
9368
- exiting,
9369
- persisting,
9370
- sharedEntering,
9371
- sharedExiting,
9372
- };
9373
- }
9374
- /**
9375
- * Check if an element is a "root" entering element (no entering ancestors)
9376
- */
9377
- function isRootEnteringElement(element, allEntering) {
9378
- let parent = element.parentElement;
9379
- while (parent) {
9380
- if (allEntering.has(parent))
9381
- return false;
9382
- parent = parent.parentElement;
9383
- }
9384
- return true;
9385
- }
9386
- /**
9387
- * Check if an element is a "root" exiting element (no exiting ancestors)
9388
- */
9389
- function isRootExitingElement(element, allExiting) {
9390
- let parent = element.parentElement;
9391
- while (parent) {
9392
- if (allExiting.has(parent))
9393
- return false;
9394
- parent = parent.parentElement;
9395
- }
9396
- return true;
9397
- }
9398
9311
 
9399
9312
  let scaleCorrectorAdded = false;
9400
9313
  /**
@@ -9468,6 +9381,11 @@
9468
9381
  transition: nodeTransition,
9469
9382
  ...options,
9470
9383
  });
9384
+ // Re-mount the node if it was previously unmounted
9385
+ // This re-adds it to root.nodes so didUpdate() will process it
9386
+ if (!existingNode.instance) {
9387
+ existingNode.mount(element);
9388
+ }
9471
9389
  return { node: existingNode, visualElement };
9472
9390
  }
9473
9391
  // No existing node - create a new one
@@ -9559,10 +9477,16 @@
9559
9477
  return "both";
9560
9478
  }
9561
9479
  /**
9562
- * Clean up projection nodes
9480
+ * Clean up projection nodes for specific elements.
9481
+ * If elementsToCleanup is provided, only those elements are cleaned up.
9482
+ * If not provided, all nodes are cleaned up.
9483
+ *
9484
+ * This allows persisting elements to keep their nodes between animations,
9485
+ * matching React's behavior where nodes persist for elements that remain in the DOM.
9563
9486
  */
9564
- function cleanupProjectionTree(context) {
9565
- for (const [element, node] of context.nodes.entries()) {
9487
+ function cleanupProjectionTree(context, elementsToCleanup) {
9488
+ const elementsToProcess = [...context.nodes.entries()];
9489
+ for (const [element, node] of elementsToProcess) {
9566
9490
  context.group.remove(node);
9567
9491
  node.unmount();
9568
9492
  // Only clear from activeProjectionNodes if this is still the active node.
@@ -9570,9 +9494,9 @@
9570
9494
  if (activeProjectionNodes.get(element) === node) {
9571
9495
  activeProjectionNodes.delete(element);
9572
9496
  }
9497
+ context.nodes.delete(element);
9498
+ context.visualElements.delete(element);
9573
9499
  }
9574
- context.nodes.clear();
9575
- context.visualElements.clear();
9576
9500
  }
9577
9501
 
9578
9502
  class LayoutAnimationBuilder {
@@ -9589,22 +9513,8 @@
9589
9513
  // Queue execution on microtask to allow builder methods to be called
9590
9514
  queueMicrotask(() => this.execute());
9591
9515
  }
9592
- enter(keyframes, options) {
9593
- this.enterKeyframes = keyframes;
9594
- this.enterOptions = options;
9595
- return this;
9596
- }
9597
- exit(keyframes, options) {
9598
- this.exitKeyframes = keyframes;
9599
- this.exitOptions = options;
9600
- return this;
9601
- }
9602
- shared(layoutIdOrOptions, options) {
9603
- if (typeof layoutIdOrOptions === "string") {
9604
- this.sharedTransitions.set(layoutIdOrOptions, options);
9605
- }
9606
- // For now, we ignore default shared options as the projection system
9607
- // handles shared transitions automatically
9516
+ shared(id, options) {
9517
+ this.sharedTransitions.set(id, options);
9608
9518
  return this;
9609
9519
  }
9610
9520
  then(onfulfilled, onrejected) {
@@ -9614,200 +9524,67 @@
9614
9524
  if (this.executed)
9615
9525
  return;
9616
9526
  this.executed = true;
9617
- const animations = [];
9618
9527
  let context;
9619
- try {
9620
- // Phase 1: Pre-mutation - Build projection tree and take snapshots
9621
- const existingElements = getLayoutElements(this.scope);
9622
- // Build projection tree for existing elements FIRST
9623
- // This allows the projection system to handle measurements correctly
9624
- if (existingElements.length > 0) {
9625
- context = buildProjectionTree(existingElements, undefined, this.getBuildOptions());
9626
- // Start update cycle
9627
- context.root.startUpdate();
9628
- // Call willUpdate on all nodes to capture snapshots via projection system
9629
- // This handles transforms, scroll, etc. correctly
9630
- for (const node of context.nodes.values()) {
9631
- // Reset isLayoutDirty so willUpdate can take a snapshot.
9632
- // When hasTreeAnimated is true on the global root, newly mounted nodes
9633
- // get isLayoutDirty=true, which causes willUpdate to skip snapshot capture.
9634
- node.isLayoutDirty = false;
9635
- node.willUpdate();
9636
- }
9637
- }
9638
- // Track DOM structure (parent, sibling) for detecting removals
9639
- // No bounds measurement here - projection system already handled that
9640
- const beforeRecords = trackLayoutElements(this.scope);
9641
- // Phase 2: Execute DOM update
9642
- this.updateDom();
9643
- // Phase 3: Post-mutation (Detect & Prepare)
9644
- const mutationResult = detectMutations(beforeRecords, this.scope);
9645
- // Reattach exiting elements that are NOT part of shared transitions
9646
- // Shared elements are handled by the projection system via resumeFrom
9647
- const nonSharedExiting = mutationResult.exiting.filter(({ element }) => {
9648
- const layoutId = element.getAttribute("data-layout-id");
9649
- return !layoutId || !mutationResult.sharedEntering.has(layoutId);
9650
- });
9651
- this.reattachExitingElements(nonSharedExiting, context);
9652
- // Build projection nodes for entering elements
9653
- if (mutationResult.entering.length > 0) {
9654
- context = buildProjectionTree(mutationResult.entering, context, this.getBuildOptions());
9655
- }
9656
- // Also ensure persisting elements have nodes if context didn't exist
9657
- if (!context && mutationResult.persisting.length > 0) {
9658
- context = buildProjectionTree(mutationResult.persisting, undefined, this.getBuildOptions());
9659
- }
9660
- // Build set of shared exiting elements to exclude from animation collection
9661
- // Their nodes are still in the tree for resumeFrom relationship, but we don't animate them
9662
- const sharedExitingElements = new Set();
9663
- for (const [layoutId] of mutationResult.sharedEntering) {
9664
- const exitingElement = mutationResult.sharedExiting.get(layoutId);
9665
- if (exitingElement) {
9666
- sharedExitingElements.add(exitingElement);
9667
- // Remove the exiting node from the shared stack so that crossfade
9668
- // doesn't trigger. When an element is removed from the DOM, it can't
9669
- // participate in crossfade (no element to fade out). The entering
9670
- // element still has resumeFrom set for position morphing.
9671
- const exitingNode = context?.nodes.get(exitingElement);
9672
- if (exitingNode) {
9673
- const stack = exitingNode.getStack();
9674
- if (stack) {
9675
- stack.remove(exitingNode);
9676
- }
9677
- }
9678
- }
9679
- }
9680
- // Phase 4: Animate
9681
- if (context) {
9682
- // Trigger layout animations via didUpdate
9683
- context.root.didUpdate();
9684
- // Wait for animations to be created (they're scheduled via frame.update)
9685
- await new Promise((resolve) => frame.postRender(() => resolve()));
9686
- // Collect layout animations from projection nodes (excluding shared exiting elements)
9687
- for (const [element, node] of context.nodes.entries()) {
9688
- if (sharedExitingElements.has(element))
9689
- continue;
9690
- if (node.currentAnimation) {
9691
- animations.push(node.currentAnimation);
9692
- }
9693
- }
9694
- // Apply enter keyframes to root entering elements
9695
- if (this.enterKeyframes) {
9696
- const enterAnimations = this.animateEntering(mutationResult, context);
9697
- animations.push(...enterAnimations);
9698
- }
9699
- // Apply exit keyframes to root exiting elements
9700
- if (this.exitKeyframes) {
9701
- const exitAnimations = this.animateExiting(mutationResult, context);
9702
- animations.push(...exitAnimations);
9703
- }
9704
- }
9705
- // Create and return group animation
9706
- const groupAnimation = new GroupAnimation(animations);
9707
- // Phase 5: Setup cleanup on complete
9708
- groupAnimation.finished.then(() => {
9709
- // Only clean up non-shared exiting elements (those we reattached)
9710
- this.cleanupExitingElements(nonSharedExiting);
9711
- if (context) {
9712
- cleanupProjectionTree(context);
9713
- }
9714
- });
9715
- this.notifyReady(groupAnimation);
9528
+ // Phase 1: Pre-mutation - Build projection tree and take snapshots
9529
+ const beforeElements = getLayoutElements(this.scope);
9530
+ if (beforeElements.length > 0) {
9531
+ context = buildProjectionTree(beforeElements, undefined, this.getBuildOptions());
9532
+ context.root.startUpdate();
9533
+ for (const node of context.nodes.values()) {
9534
+ node.isLayoutDirty = false;
9535
+ node.willUpdate();
9536
+ }
9537
+ }
9538
+ // Phase 2: Execute DOM update
9539
+ this.updateDom();
9540
+ // Phase 3: Post-mutation - Compare before/after elements
9541
+ const afterElements = getLayoutElements(this.scope);
9542
+ const beforeSet = new Set(beforeElements);
9543
+ const afterSet = new Set(afterElements);
9544
+ const entering = afterElements.filter((el) => !beforeSet.has(el));
9545
+ const exiting = beforeElements.filter((el) => !afterSet.has(el));
9546
+ // Build projection nodes for entering elements
9547
+ if (entering.length > 0) {
9548
+ context = buildProjectionTree(entering, context, this.getBuildOptions());
9549
+ }
9550
+ // No layout elements - return empty animation
9551
+ if (!context) {
9552
+ this.notifyReady(new GroupAnimation([]));
9553
+ return;
9716
9554
  }
9717
- catch (error) {
9718
- // Cleanup on error
9719
- if (context) {
9720
- cleanupProjectionTree(context);
9721
- }
9722
- throw error;
9555
+ // Handle shared elements
9556
+ for (const element of exiting) {
9557
+ const node = context.nodes.get(element);
9558
+ node?.getStack()?.remove(node);
9723
9559
  }
9724
- }
9725
- getBuildOptions() {
9726
- return {
9727
- defaultTransition: this.defaultOptions || { duration: 0.3, ease: "easeOut" },
9728
- sharedTransitions: this.sharedTransitions.size > 0 ? this.sharedTransitions : undefined,
9729
- };
9730
- }
9731
- reattachExitingElements(exiting, context) {
9732
- for (const { element, parentElement, nextSibling } of exiting) {
9733
- // Check if parent still exists in DOM
9734
- if (!parentElement.isConnected)
9735
- continue;
9736
- // Get bounds from projection node snapshot (measured correctly via projection system)
9737
- const node = context?.nodes.get(element);
9738
- const snapshot = node?.snapshot;
9739
- if (!snapshot)
9740
- continue;
9741
- const { layoutBox } = snapshot;
9742
- // Reattach element
9743
- if (nextSibling && nextSibling.parentNode === parentElement) {
9744
- parentElement.insertBefore(element, nextSibling);
9745
- }
9746
- else {
9747
- parentElement.appendChild(element);
9748
- }
9749
- // Apply absolute positioning to prevent layout shift
9750
- // Use layoutBox from projection system which has transform-free measurements
9751
- const htmlElement = element;
9752
- htmlElement.style.position = "absolute";
9753
- htmlElement.style.top = `${layoutBox.y.min}px`;
9754
- htmlElement.style.left = `${layoutBox.x.min}px`;
9755
- htmlElement.style.width = `${layoutBox.x.max - layoutBox.x.min}px`;
9756
- htmlElement.style.height = `${layoutBox.y.max - layoutBox.y.min}px`;
9757
- htmlElement.style.margin = "0";
9758
- htmlElement.style.pointerEvents = "none";
9560
+ for (const element of entering) {
9561
+ context.nodes.get(element)?.promote();
9759
9562
  }
9760
- }
9761
- cleanupExitingElements(exiting) {
9762
- for (const { element } of exiting) {
9763
- if (element.parentElement) {
9764
- element.parentElement.removeChild(element);
9765
- }
9766
- }
9767
- }
9768
- animateEntering(mutationResult, context) {
9769
- const enteringSet = new Set(mutationResult.entering);
9770
- // Find root entering elements
9771
- const rootEntering = mutationResult.entering.filter((el) => isRootEnteringElement(el, enteringSet));
9563
+ // Phase 4: Animate
9564
+ context.root.didUpdate();
9565
+ await new Promise((resolve) => frame.postRender(() => resolve()));
9772
9566
  const animations = [];
9773
- for (const element of rootEntering) {
9774
- const visualElement = context.visualElements.get(element);
9775
- if (!visualElement)
9776
- continue;
9777
- // If entering with opacity: 1, start from opacity: 0
9778
- const keyframes = { ...this.enterKeyframes };
9779
- if (keyframes.opacity !== undefined) {
9780
- const targetOpacity = Array.isArray(keyframes.opacity)
9781
- ? keyframes.opacity[keyframes.opacity.length - 1]
9782
- : keyframes.opacity;
9783
- if (targetOpacity === 1) {
9784
- element.style.opacity = "0";
9785
- }
9567
+ for (const node of context.nodes.values()) {
9568
+ if (node.currentAnimation) {
9569
+ animations.push(node.currentAnimation);
9786
9570
  }
9787
- const options = this.enterOptions || this.defaultOptions || {};
9788
- const enterAnims = animateTarget(visualElement, keyframes, {
9789
- transitionOverride: options,
9790
- });
9791
- animations.push(...enterAnims);
9792
9571
  }
9793
- return animations;
9572
+ const groupAnimation = new GroupAnimation(animations);
9573
+ groupAnimation.finished.then(() => {
9574
+ cleanupProjectionTree(context);
9575
+ });
9576
+ this.notifyReady(groupAnimation);
9794
9577
  }
9795
- animateExiting(mutationResult, context) {
9796
- const exitingSet = new Set(mutationResult.exiting.map((r) => r.element));
9797
- // Find root exiting elements
9798
- const rootExiting = mutationResult.exiting.filter((r) => isRootExitingElement(r.element, exitingSet));
9799
- const animations = [];
9800
- for (const { element } of rootExiting) {
9801
- const visualElement = context.visualElements.get(element);
9802
- if (!visualElement)
9803
- continue;
9804
- const options = this.exitOptions || this.defaultOptions || {};
9805
- const exitAnims = animateTarget(visualElement, this.exitKeyframes, {
9806
- transitionOverride: options,
9807
- });
9808
- animations.push(...exitAnims);
9809
- }
9810
- return animations;
9578
+ getBuildOptions() {
9579
+ return {
9580
+ defaultTransition: this.defaultOptions || {
9581
+ duration: 0.3,
9582
+ ease: "easeOut",
9583
+ },
9584
+ sharedTransitions: this.sharedTransitions.size > 0
9585
+ ? this.sharedTransitions
9586
+ : undefined,
9587
+ };
9811
9588
  }
9812
9589
  }
9813
9590
  /**
@@ -9832,47 +9609,6 @@
9832
9609
  };
9833
9610
  }
9834
9611
 
9835
- /**
9836
- * Animate layout changes within a DOM tree.
9837
- *
9838
- * @example
9839
- * ```typescript
9840
- * // Basic usage - animates all elements with data-layout or data-layout-id
9841
- * await animateLayout(() => {
9842
- * container.innerHTML = newContent
9843
- * })
9844
- *
9845
- * // With scope - only animates within the container
9846
- * await animateLayout(".container", () => {
9847
- * updateContent()
9848
- * })
9849
- *
9850
- * // With options
9851
- * await animateLayout(() => update(), { duration: 0.5 })
9852
- *
9853
- * // Builder pattern for enter/exit animations
9854
- * animateLayout(".cards", () => {
9855
- * container.innerHTML = newCards
9856
- * }, { duration: 0.3 })
9857
- * .enter({ opacity: 1, scale: 1 }, { duration: 0.2 })
9858
- * .exit({ opacity: 0, scale: 0.8 })
9859
- * .shared("hero", { type: "spring" })
9860
- * ```
9861
- *
9862
- * Elements are animated if they have:
9863
- * - `data-layout` attribute (layout animation only)
9864
- * - `data-layout-id` attribute (shared element transitions)
9865
- *
9866
- * @param scopeOrUpdateDom - Either a scope selector/element, or the DOM update function
9867
- * @param updateDomOrOptions - Either the DOM update function or animation options
9868
- * @param options - Animation options (when scope is provided)
9869
- * @returns A builder that resolves to animation controls
9870
- */
9871
- function unstable_animateLayout(scopeOrUpdateDom, updateDomOrOptions, options) {
9872
- const { scope, updateDom, defaultOptions } = parseAnimateLayoutArgs(scopeOrUpdateDom, updateDomOrOptions, options);
9873
- return new LayoutAnimationBuilder(scope, updateDom, defaultOptions);
9874
- }
9875
-
9876
9612
  /**
9877
9613
  * @deprecated
9878
9614
  *
@@ -11164,6 +10900,7 @@
11164
10900
  exports.observeTimeline = observeTimeline;
11165
10901
  exports.optimizedAppearDataAttribute = optimizedAppearDataAttribute;
11166
10902
  exports.optimizedAppearDataId = optimizedAppearDataId;
10903
+ exports.parseAnimateLayoutArgs = parseAnimateLayoutArgs;
11167
10904
  exports.parseCSSVariable = parseCSSVariable;
11168
10905
  exports.parseValueFromTransform = parseValueFromTransform;
11169
10906
  exports.percent = percent;
@@ -11233,7 +10970,6 @@
11233
10970
  exports.transformValue = transformValue;
11234
10971
  exports.transformValueTypes = transformValueTypes;
11235
10972
  exports.translateAxis = translateAxis;
11236
- exports.unstable_animateLayout = unstable_animateLayout;
11237
10973
  exports.updateMotionValuesFromProps = updateMotionValuesFromProps;
11238
10974
  exports.variantPriorityOrder = variantPriorityOrder;
11239
10975
  exports.variantProps = variantProps;