motion 13.4.2 → 13.4.4

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.
package/README.md CHANGED
@@ -24,10 +24,12 @@ npm install motion-v
24
24
  1. [Why Motion?](#why-motion)
25
25
  2. [🍦 Platforms](#-platforms)
26
26
  3. [🎓 Examples](#-examples)
27
- 4. [⚡️ Motion+](#-motion)
28
- 5. [👩🏻‍⚖️ License](#-license)
29
- 6. [💎 Contribute](#-contribute)
30
- 7. [✨ Sponsors](#-sponsors)
27
+ 4. [🎨 Motion Studio](#-motion-studio)
28
+ 4. [🤖 Using Motion with AI](#-using-motion-with-ai)
29
+ 5. [⚡️ Motion+](#-motion)
30
+ 6. [👩🏻‍⚖️ License](#-license)
31
+ 7. [💎 Contribute](#-contribute)
32
+ 8. [✨ Sponsors](#-sponsors)
31
33
 
32
34
  ## Why Motion?
33
35
 
@@ -106,19 +108,35 @@ Get started with [Motion for Vue](https://motion.dev/docs/vue).
106
108
 
107
109
  ## 🎓 Examples & tutorials
108
110
 
109
- Browse 330+ [official examples](https://motion.dev/examples), with copy-paste code that'll level-up your animations whether you're a beginner or an expert.
111
+ Browse 450+ [official examples](https://motion.dev/examples), with copy-paste code that'll level-up your animations whether you're a beginner or an expert.
110
112
 
111
- Over 100 examples come with a full step-by-step [tutorial](https://motion.dev/tutorials).
113
+ Over 110 examples come with a full step-by-step tutorial on [their example page](https://motion.dev/examples).
114
+
115
+ ## 🤖 Using Motion with AI
116
+
117
+ Give your coding agent current Motion docs:
118
+
119
+ - **Agent skill:** `npx motion-ai` installs the free, MIT-licensed `/motion` skill and sets up Motion's MCP servers for Claude Code, Cursor, Amp, OpenCode, Gemini CLI and Copilot. [Source on GitHub](https://github.com/motiondivision/ai-kit).
120
+ - **MCP server:** `https://mcp.motion.dev` searches the Motion docs and example metadata. It is free and needs no account.
121
+ - **llms.txt:** [motion.dev/llms.txt](https://motion.dev/llms.txt?utm_source=npm-readme) indexes every docs page. Motion+ pages are labelled as paid, with their install and import.
122
+
123
+ [Motion+](https://motion.dev/plus?utm_source=npm-readme) adds example and Motion UI source, MotionScore performance audits and CSS spring generation for your agent.
124
+
125
+ ## 🎨 Motion Studio
126
+
127
+ A visual animation editor for your website. Edit keyframes, easing curves and springs on a live timeline, or describe changes to the Ultramotion agent, then apply the result straight to your code with Cursor, Codex or Claude.
128
+
129
+ [Explore Motion Studio](https://motion.dev/studio)
112
130
 
113
131
  ## ⚡️ Motion+
114
132
 
115
- A one-time payment, lifetime-updates membership:
133
+ A one-time Personal licence with lifetime updates, or an annual per-seat Business plan for teams:
116
134
 
117
- - **330+ examples**
118
- - **100+ tutorials**
135
+ - **450+ examples**
136
+ - **110+ tutorials**
119
137
  - **Premium APIs** like [Cursor](https://motion.dev/docs/cursor) and [Ticker](https://motion.dev/docs/react-ticker)
120
138
  - **Transition editor** for Cursor and VS Code
121
- - **AI skills**
139
+ - **AI Kit:** example and Motion UI source, MotionScore audits and CSS springs for your agent
122
140
  - **Private Discord**
123
141
  - **Early access content**
124
142
 
@@ -1225,22 +1225,50 @@
1225
1225
  }
1226
1226
  }
1227
1227
  const durationKeys = ["duration", "bounce"];
1228
- const physicsKeys = ["stiffness", "damping", "mass"];
1229
1228
  function isSpringType(options, keys) {
1230
1229
  return keys.some((key) => options[key] !== undefined);
1231
1230
  }
1231
+ /**
1232
+ * Spring physics must be finite. stiffness and mass are also divisors so must
1233
+ * be positive, whereas a damping of 0 is a valid, perpetually oscillating
1234
+ * spring. Relational rather than Number.isFinite so numeric strings still
1235
+ * coerce.
1236
+ */
1237
+ const isValidPhysics = (value, canBeZero) => (canBeZero ? value >= 0 : value > 0) && value < Infinity;
1238
+ /**
1239
+ * Returns value if it's usable spring physics, otherwise undefined so callers
1240
+ * fall back to the default. An explicit `undefined`, e.g. from a forwarded
1241
+ * optional prop, must fall back too. Invalid physics would otherwise resolve
1242
+ * to NaN spring values that never report done.
1243
+ */
1244
+ function resolvePhysics(value, canBeZero) {
1245
+ if (isValidPhysics(value, canBeZero))
1246
+ return value;
1247
+ {
1248
+ exports.warning(value === undefined, "Spring stiffness and mass must be positive, damping 0 or greater", "spring-invalid-physics");
1249
+ }
1250
+ return undefined;
1251
+ }
1232
1252
  function getSpringOptions(options) {
1253
+ /**
1254
+ * Resolve physics before choosing between physics- and duration-based
1255
+ * resolution, so an invalid stiffness doesn't also silently discard a
1256
+ * valid duration/bounce.
1257
+ */
1258
+ const validStiffness = resolvePhysics(options.stiffness);
1259
+ const validDamping = resolvePhysics(options.damping, true);
1260
+ const validMass = resolvePhysics(options.mass);
1233
1261
  let springOptions = {
1234
- velocity: springDefaults.velocity,
1235
- stiffness: springDefaults.stiffness,
1236
- damping: springDefaults.damping,
1237
- mass: springDefaults.mass,
1238
- isResolvedFromDuration: false,
1239
1262
  ...options,
1263
+ stiffness: validStiffness ?? springDefaults.stiffness,
1264
+ damping: validDamping ?? springDefaults.damping,
1265
+ mass: validMass ?? springDefaults.mass,
1266
+ isResolvedFromDuration: false,
1267
+ // stiffness/damping/mass overrides duration/bounce
1268
+ isTimeDefined: (validStiffness ?? validDamping ?? validMass) === undefined &&
1269
+ isSpringType(options, durationKeys),
1240
1270
  };
1241
- // stiffness/damping/mass overrides duration/bounce
1242
- if (!isSpringType(options, physicsKeys) &&
1243
- isSpringType(options, durationKeys)) {
1271
+ if (springOptions.isTimeDefined) {
1244
1272
  // Time-defined springs should ignore inherited velocity.
1245
1273
  // Velocity from interrupted animations can cause findSpring()
1246
1274
  // to compute wildly different spring parameters, leading to
@@ -1261,7 +1289,7 @@
1261
1289
  };
1262
1290
  }
1263
1291
  else {
1264
- const derived = findSpring({ ...options, velocity: 0 });
1292
+ const derived = findSpring(springOptions);
1265
1293
  springOptions = {
1266
1294
  ...springOptions,
1267
1295
  ...derived,
@@ -1269,6 +1297,17 @@
1269
1297
  };
1270
1298
  springOptions.isResolvedFromDuration = true;
1271
1299
  }
1300
+ /**
1301
+ * Non-finite time options degenerate: a NaN bounce gives a NaN
1302
+ * damping, an infinite visualDuration a 0 stiffness. Replace the two
1303
+ * together, so the relationship duration resolution establishes
1304
+ * between them is never left half-overwritten.
1305
+ */
1306
+ if (!isValidPhysics(springOptions.stiffness) ||
1307
+ !isValidPhysics(springOptions.damping, true)) {
1308
+ springOptions.stiffness = springDefaults.stiffness;
1309
+ springOptions.damping = springDefaults.damping;
1310
+ }
1272
1311
  }
1273
1312
  return springOptions;
1274
1313
  }
@@ -1287,7 +1326,7 @@
1287
1326
  * to reduce GC during animation.
1288
1327
  */
1289
1328
  const state = { done: false, value: origin };
1290
- const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, } = getSpringOptions({
1329
+ const { stiffness, damping, mass, duration, velocity, isResolvedFromDuration, isTimeDefined, } = getSpringOptions({
1291
1330
  ...options,
1292
1331
  velocity: -millisecondsToSeconds(options.velocity || 0),
1293
1332
  });
@@ -1312,7 +1351,7 @@
1312
1351
  * If we're working on a granular scale, use smaller defaults for determining
1313
1352
  * when the spring is finished.
1314
1353
  *
1315
- * These defaults have been selected emprically based on what strikes a good
1354
+ * These defaults have been selected empirically based on what strikes a good
1316
1355
  * ratio between feeling good and finishing as soon as changes are imperceptible.
1317
1356
  */
1318
1357
  const setRestThresholds = () => {
@@ -1410,11 +1449,6 @@
1410
1449
  };
1411
1450
  }
1412
1451
  update();
1413
- /**
1414
- * Time-defined springs ignore inherited velocity, see getSpringOptions.
1415
- */
1416
- const ignoreVelocity = !isSpringType(options, physicsKeys) &&
1417
- isSpringType(options, durationKeys);
1418
1452
  const calculatedDuration = isResolvedFromDuration ? duration || null : null;
1419
1453
  const generator = {
1420
1454
  calculatedDuration,
@@ -1425,9 +1459,8 @@
1425
1459
  retarget: (keyframes, newVelocity) => {
1426
1460
  s.target = keyframes[keyframes.length - 1];
1427
1461
  s.delta = s.target - keyframes[0];
1428
- s.velocity = ignoreVelocity
1429
- ? 0
1430
- : -millisecondsToSeconds(newVelocity);
1462
+ // Time-defined springs ignore inherited velocity, see getSpringOptions
1463
+ s.velocity = isTimeDefined ? 0 : -millisecondsToSeconds(newVelocity);
1431
1464
  // Default thresholds depend on the scale of the new delta
1432
1465
  if (!(options.restSpeed && options.restDelta))
1433
1466
  setRestThresholds();
@@ -3445,7 +3478,7 @@
3445
3478
 
3446
3479
  class GroupAnimation {
3447
3480
  constructor(animations) {
3448
- // Bound to accomadate common `return animation.stop` pattern
3481
+ // Bound to accommodate common `return animation.stop` pattern
3449
3482
  this.stop = () => this.runAll("stop");
3450
3483
  this.animations = animations.filter(Boolean);
3451
3484
  }
@@ -5777,7 +5810,7 @@
5777
5810
  translateAxis(box.y, -node.scroll.offset.y);
5778
5811
  }
5779
5812
  if (delta) {
5780
- // Incoporate each ancestor's scale into a cumulative treeScale for this component
5813
+ // Incorporate each ancestor's scale into a cumulative treeScale for this component
5781
5814
  treeScale.x *= delta.x.scale;
5782
5815
  treeScale.y *= delta.y.scale;
5783
5816
  // Apply each ancestor's calculated delta into this component's recorded layout box
@@ -5985,7 +6018,7 @@
5985
6018
  /**
5986
6019
  * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
5987
6020
  * in that it has an easier syntax, filters out polyfilled touch events, interoperates
5988
- * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
6021
+ * with drag gestures, and automatically removes the "pointerleave" event listener when the hover ends.
5989
6022
  *
5990
6023
  * @public
5991
6024
  */
@@ -7721,7 +7754,7 @@
7721
7754
  /**
7722
7755
  * Iterate backwards over the builders array. We can ignore the
7723
7756
  * "wait" animations. If we have an interrupting animation in the
7724
- * queue then we need to batch all preceeding animations into it.
7757
+ * queue then we need to batch all preceding animations into it.
7725
7758
  * Currently this only batches the update functions but will also
7726
7759
  * need to batch the targets.
7727
7760
  */
@@ -8601,7 +8634,7 @@
8601
8634
  /**
8602
8635
  * compareDocumentPosition returns a bitmask, by using the bitwise &
8603
8636
  * we're returning true if 2 in that bitmask is set to true. 2 is set
8604
- * to true if b preceeds a.
8637
+ * to true if b precedes a.
8605
8638
  */
8606
8639
  return a.compareDocumentPosition(b) & 2 ? 1 : -1;
8607
8640
  }
@@ -9093,7 +9126,7 @@
9093
9126
  }
9094
9127
  /**
9095
9128
  * Set all encountered keys so far as the protected keys for this type. This will
9096
- * be any key that has been animated or otherwise handled by active, higher-priortiy types.
9129
+ * be any key that has been animated or otherwise handled by active, higher-priority types.
9097
9130
  */
9098
9131
  typeState.protectedKeys = { ...encounteredKeys };
9099
9132
  // Check if we can skip analysing this prop early
@@ -9918,8 +9951,8 @@
9918
9951
  this.hasCheckedOptimisedAppear = false;
9919
9952
  /**
9920
9953
  * An object representing the calculated contextual/accumulated/tree scale.
9921
- * This will be used to scale calculcated projection transforms, as these are
9922
- * calculated in screen-space but need to be scaled for elements to layoutly
9954
+ * This will be used to scale calculated projection transforms, as these are
9955
+ * calculated in screen-space but need to be scaled for elements to visually
9923
9956
  * make it to their calculated destinations.
9924
9957
  *
9925
9958
  * TODO: Lazy-init
@@ -12561,7 +12594,10 @@
12561
12594
  x: createAxisInfo(),
12562
12595
  y: createAxisInfo(),
12563
12596
  });
12564
- const keys = {
12597
+ /**
12598
+ * Also iterated with for...in as the list of axes.
12599
+ */
12600
+ const axisKeys = {
12565
12601
  x: {
12566
12602
  length: "Width",
12567
12603
  position: "Left",
@@ -12573,11 +12609,13 @@
12573
12609
  };
12574
12610
  function updateAxisInfo(element, axisName, info, time) {
12575
12611
  const axis = info[axisName];
12576
- const { length, position } = keys[axisName];
12612
+ const { length, position } = axisKeys[axisName];
12577
12613
  const prev = axis.current;
12578
12614
  const prevTime = info.time;
12579
12615
  axis.current = Math.abs(element[`scroll${position}`]);
12580
- axis.scrollLength = element[`scroll${length}`] - element[`client${length}`];
12616
+ axis.containerLength = element[`client${length}`];
12617
+ axis.targetLength = element[`scroll${length}`];
12618
+ axis.scrollLength = axis.targetLength - axis.containerLength;
12581
12619
  axis.offset.length = 0;
12582
12620
  axis.offset[0] = 0;
12583
12621
  axis.offset[1] = axis.scrollLength;
@@ -12588,54 +12626,15 @@
12588
12626
  ? 0
12589
12627
  : velocityPerSecond(axis.current - prev, elapsed);
12590
12628
  }
12629
+ /**
12630
+ * Measures a scroll container. Runs once per container per frame; every
12631
+ * handler on that container derives its info from the result.
12632
+ */
12591
12633
  function updateScrollInfo(element, info, time) {
12592
- updateAxisInfo(element, "x", info, time);
12593
- updateAxisInfo(element, "y", info, time);
12594
- info.time = time;
12595
- }
12596
-
12597
- function calcInset(element, container) {
12598
- const inset = { x: 0, y: 0 };
12599
- let current = element;
12600
- while (current && current !== container) {
12601
- if (isHTMLElement(current)) {
12602
- inset.x += current.offsetLeft;
12603
- inset.y += current.offsetTop;
12604
- current = current.offsetParent;
12605
- }
12606
- else if (current.tagName === "svg") {
12607
- /**
12608
- * This isn't an ideal approach to measuring the offset of <svg /> tags.
12609
- * It would be preferable, given they behave like HTMLElements in most ways
12610
- * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
12611
- * can't use .getBBox() like most SVG elements as these provide the offset
12612
- * relative to the SVG itself, which for <svg /> is usually 0x0.
12613
- */
12614
- const svgBoundingBox = current.getBoundingClientRect();
12615
- current = current.parentElement;
12616
- const parentBoundingBox = current.getBoundingClientRect();
12617
- inset.x += svgBoundingBox.left - parentBoundingBox.left;
12618
- inset.y += svgBoundingBox.top - parentBoundingBox.top;
12619
- }
12620
- else if (current instanceof SVGGraphicsElement) {
12621
- const { x, y } = current.getBBox();
12622
- inset.x += x;
12623
- inset.y += y;
12624
- let svg = null;
12625
- let parent = current.parentNode;
12626
- while (!svg) {
12627
- if (parent.tagName === "svg") {
12628
- svg = parent;
12629
- }
12630
- parent = current.parentNode;
12631
- }
12632
- current = svg;
12633
- }
12634
- else {
12635
- break;
12636
- }
12634
+ for (const axis in axisKeys) {
12635
+ updateAxisInfo(element, axis, info, time);
12637
12636
  }
12638
- return inset;
12637
+ info.time = time;
12639
12638
  }
12640
12639
 
12641
12640
  const namedEdges = {
@@ -12733,86 +12732,104 @@
12733
12732
  ],
12734
12733
  };
12735
12734
 
12736
- const point = { x: 0, y: 0 };
12737
- function getTargetSize(target) {
12738
- return "getBBox" in target && target.tagName !== "svg"
12739
- ? target.getBBox()
12740
- : { width: target.clientWidth, height: target.clientHeight };
12735
+ /**
12736
+ * Resolved offsets map to evenly spaced progress values, so progress is
12737
+ * derived from the segment index rather than building an interpolator.
12738
+ */
12739
+ function offsetsToProgress(offsets, v) {
12740
+ const n = offsets.length - 1;
12741
+ if (n < 1)
12742
+ return 0;
12743
+ const reverse = offsets[0] > offsets[n];
12744
+ const at = (i) => offsets[reverse ? n - i : i];
12745
+ /**
12746
+ * Matches interpolate(), which checks for a zero-length first range
12747
+ * before reversing descending offsets.
12748
+ */
12749
+ if (offsets[0] === offsets[1] && v < at(0))
12750
+ return reverse ? 1 : 0;
12751
+ let i = 0;
12752
+ while (i < n - 1 && v >= at(i + 1))
12753
+ i++;
12754
+ const p = (i + progress(at(i), at(i + 1), v)) / n;
12755
+ return reverse ? 1 - p : p;
12741
12756
  }
12742
- function resolveOffsets(container, info, options) {
12743
- const { offset: offsetDefinition = ScrollOffset.All } = options;
12744
- const { target = container, axis = "y" } = options;
12745
- const lengthLabel = axis === "y" ? "height" : "width";
12746
- const inset = target !== container ? calcInset(target, container) : point;
12747
- /**
12748
- * Measure the target and container. If they're the same thing then we
12749
- * use the container's scrollWidth/Height as the target, from there
12750
- * all other calculations can remain the same.
12751
- */
12752
- const targetSize = target === container
12753
- ? { width: container.scrollWidth, height: container.scrollHeight }
12754
- : getTargetSize(target);
12755
- const containerSize = {
12756
- width: container.clientWidth,
12757
- height: container.clientHeight,
12758
- };
12757
+ function resolveOffsets(info, options) {
12758
+ const { offset: offsetDefinition = ScrollOffset.All, axis = "y" } = options;
12759
+ const axisInfo = info[axis];
12759
12760
  /**
12760
12761
  * Reset the length of the resolved offset array rather than creating a new one.
12761
- * TODO: More reusable data structures for targetSize/containerSize would also be good.
12762
12762
  */
12763
- info[axis].offset.length = 0;
12763
+ axisInfo.offset.length = 0;
12764
12764
  /**
12765
12765
  * Populate the offset array by resolving the user's offset definition into
12766
- * a list of pixel scroll offets.
12766
+ * a list of pixel scroll offsets.
12767
12767
  */
12768
- let hasChanged = !info[axis].interpolate;
12769
12768
  const numOffsets = offsetDefinition.length;
12770
12769
  for (let i = 0; i < numOffsets; i++) {
12771
- const offset = resolveOffset(offsetDefinition[i], containerSize[lengthLabel], targetSize[lengthLabel], inset[axis]);
12772
- if (!hasChanged && offset !== info[axis].interpolatorOffsets[i]) {
12773
- hasChanged = true;
12774
- }
12775
- info[axis].offset[i] = offset;
12776
- }
12777
- /**
12778
- * If the pixel scroll offsets have changed, create a new interpolator function
12779
- * to map scroll value into a progress.
12780
- */
12781
- if (hasChanged) {
12782
- info[axis].interpolate = interpolate(info[axis].offset, defaultOffset$1(offsetDefinition), { clamp: false });
12783
- info[axis].interpolatorOffsets = [...info[axis].offset];
12770
+ axisInfo.offset[i] = resolveOffset(offsetDefinition[i], axisInfo.containerLength, axisInfo.targetLength, axisInfo.targetOffset);
12784
12771
  }
12785
- info[axis].progress = clamp(0, 1, info[axis].interpolate(info[axis].current));
12772
+ axisInfo.progress = clamp(0, 1, offsetsToProgress(axisInfo.offset, axisInfo.current));
12786
12773
  }
12787
12774
 
12788
- function measure(container, target = container, info) {
12789
- /**
12790
- * Find inset of target within scrollable container
12791
- */
12792
- info.x.targetOffset = 0;
12793
- info.y.targetOffset = 0;
12794
- if (target !== container) {
12795
- let node = target;
12796
- while (node && node !== container) {
12797
- info.x.targetOffset += node.offsetLeft;
12798
- info.y.targetOffset += node.offsetTop;
12799
- node = node.offsetParent;
12775
+ function calcInset(element, container) {
12776
+ const inset = { x: 0, y: 0 };
12777
+ let current = element;
12778
+ while (current && current !== container) {
12779
+ if (isHTMLElement(current)) {
12780
+ inset.x += current.offsetLeft;
12781
+ inset.y += current.offsetTop;
12782
+ current = current.offsetParent;
12783
+ }
12784
+ else if (current.tagName === "svg") {
12785
+ /**
12786
+ * This isn't an ideal approach to measuring the offset of <svg /> tags.
12787
+ * It would be preferable, given they behave like HTMLElements in most ways
12788
+ * to use offsetLeft/Top. But these don't exist on <svg />. Likewise we
12789
+ * can't use .getBBox() like most SVG elements as these provide the offset
12790
+ * relative to the SVG itself, which for <svg /> is usually 0x0.
12791
+ */
12792
+ const svgBoundingBox = current.getBoundingClientRect();
12793
+ current = current.parentElement;
12794
+ const parentBoundingBox = current.getBoundingClientRect();
12795
+ inset.x += svgBoundingBox.left - parentBoundingBox.left;
12796
+ inset.y += svgBoundingBox.top - parentBoundingBox.top;
12797
+ }
12798
+ else if (current instanceof SVGGraphicsElement) {
12799
+ const { x, y } = current.getBBox();
12800
+ inset.x += x;
12801
+ inset.y += y;
12802
+ let svg = null;
12803
+ let parent = current.parentNode;
12804
+ while (!svg) {
12805
+ if (parent.tagName === "svg") {
12806
+ svg = parent;
12807
+ }
12808
+ parent = current.parentNode;
12809
+ }
12810
+ current = svg;
12811
+ }
12812
+ else {
12813
+ break;
12800
12814
  }
12801
12815
  }
12802
- info.x.targetLength =
12803
- target === container ? target.scrollWidth : target.clientWidth;
12804
- info.y.targetLength =
12805
- target === container ? target.scrollHeight : target.clientHeight;
12806
- info.x.containerLength = container.clientWidth;
12807
- info.y.containerLength = container.clientHeight;
12816
+ return inset;
12817
+ }
12818
+
12819
+ function getTargetSize(target) {
12820
+ return "getBBox" in target && target.tagName !== "svg"
12821
+ ? target.getBBox()
12822
+ : { width: target.clientWidth, height: target.clientHeight };
12823
+ }
12824
+ function createOnScrollHandler(container, onScroll, info, options = {}) {
12825
+ const { target } = options;
12808
12826
  /**
12809
12827
  * In development mode ensure scroll containers aren't position: static as this makes
12810
12828
  * it difficult to measure their relative positions. The document scrolling element
12811
12829
  * is exempt: offsetParent measurements naturally resolve relative to the document.
12812
12830
  */
12813
12831
  {
12814
- if (container &&
12815
- target &&
12832
+ if (target &&
12816
12833
  target !== container &&
12817
12834
  container !== document.documentElement &&
12818
12835
  container !== document.scrollingElement &&
@@ -12820,24 +12837,39 @@
12820
12837
  warnOnce(getComputedStyle(container).position !== "static", "Please ensure that the container has a non-static position, like 'relative', 'fixed', or 'absolute' to ensure scroll offset is calculated correctly.");
12821
12838
  }
12822
12839
  }
12823
- }
12824
- function createOnScrollHandler(element, onScroll, info, options = {}) {
12840
+ /**
12841
+ * Handlers without a target or offset are notified with the container's
12842
+ * shared info object, so they measure nothing themselves.
12843
+ */
12844
+ const needsOwnInfo = target || options.offset;
12825
12845
  return {
12826
- measure: (time) => {
12827
- measure(element, options.target, info);
12828
- updateScrollInfo(element, info, time);
12829
- if (options.offset || options.target) {
12830
- resolveOffsets(element, info, options);
12831
- }
12846
+ measure: (containerInfo) => {
12847
+ if (!needsOwnInfo)
12848
+ return;
12849
+ info.time = containerInfo.time;
12850
+ for (const key in axisKeys) {
12851
+ const axis = key;
12852
+ const { offset } = info[axis];
12853
+ Object.assign(info[axis], containerInfo[axis]);
12854
+ info[axis].offset = offset;
12855
+ }
12856
+ if (target && target !== container) {
12857
+ const inset = calcInset(target, container);
12858
+ const size = getTargetSize(target);
12859
+ info.x.targetOffset = inset.x;
12860
+ info.y.targetOffset = inset.y;
12861
+ info.x.targetLength = size.width;
12862
+ info.y.targetLength = size.height;
12863
+ }
12864
+ resolveOffsets(info, options);
12832
12865
  },
12833
- notify: () => onScroll(info),
12866
+ notify: (containerInfo) => onScroll(needsOwnInfo ? info : containerInfo),
12834
12867
  };
12835
12868
  }
12836
12869
 
12837
12870
  const scrollListeners = new WeakMap();
12838
12871
  const resizeListeners = new WeakMap();
12839
12872
  const onScrollHandlers = new WeakMap();
12840
- const scrollSize = new WeakMap();
12841
12873
  const dimensionCheckProcesses = new WeakMap();
12842
12874
  const getEventTarget = (element) => element === document.scrollingElement ? window : element;
12843
12875
  function scrollInfo(onScroll, { container = document.scrollingElement, trackContentSize = false, ...options } = {}) {
@@ -12863,15 +12895,17 @@
12863
12895
  * If not, create one.
12864
12896
  */
12865
12897
  if (!scrollListeners.has(container)) {
12898
+ const containerInfo = createScrollInfo();
12866
12899
  const measureAll = () => {
12900
+ updateScrollInfo(container, containerInfo, frameData.timestamp);
12867
12901
  for (const handler of containerHandlers) {
12868
- handler.measure(frameData.timestamp);
12902
+ handler.measure(containerInfo);
12869
12903
  }
12870
12904
  frame.preUpdate(notifyAll);
12871
12905
  };
12872
12906
  const notifyAll = () => {
12873
12907
  for (const handler of containerHandlers) {
12874
- handler.notify();
12908
+ handler.notify(containerInfo);
12875
12909
  }
12876
12910
  };
12877
12911
  const listener = () => frame.read(measureAll);
@@ -12894,7 +12928,6 @@
12894
12928
  width: container.scrollWidth,
12895
12929
  height: container.scrollHeight,
12896
12930
  };
12897
- scrollSize.set(container, size);
12898
12931
  // Add frame-based scroll dimension checking to detect content changes
12899
12932
  const checkScrollDimensions = () => {
12900
12933
  const newWidth = container.scrollWidth;
@@ -12923,8 +12956,11 @@
12923
12956
  if (currentHandlers.size)
12924
12957
  return;
12925
12958
  /**
12926
- * If no more handlers, remove the scroll listener too.
12959
+ * If no more handlers, remove the scroll listener too. The handler
12960
+ * set goes with it, as a measure still queued from this listener
12961
+ * would otherwise notify handlers added by a later scrollInfo call.
12927
12962
  */
12963
+ onScrollHandlers.delete(container);
12928
12964
  const scrollListener = scrollListeners.get(container);
12929
12965
  scrollListeners.delete(container);
12930
12966
  if (scrollListener) {
@@ -12938,7 +12974,6 @@
12938
12974
  cancelFrame(dimensionCheckProcess);
12939
12975
  dimensionCheckProcesses.delete(container);
12940
12976
  }
12941
- scrollSize.delete(container);
12942
12977
  };
12943
12978
  }
12944
12979
 
@@ -13019,10 +13054,8 @@
13019
13054
  }, options);
13020
13055
  return { currentTime, cancel };
13021
13056
  }
13022
- function getTimeline({ source, container, ...options }) {
13057
+ function getTimeline({ container, ...options }) {
13023
13058
  const { axis } = options;
13024
- if (source)
13025
- container = source;
13026
13059
  let containerCache = timelineCache.get(container);
13027
13060
  if (!containerCache) {
13028
13061
  containerCache = new Map();
@@ -13098,38 +13131,16 @@
13098
13131
  });
13099
13132
  }
13100
13133
 
13101
- /**
13102
- * Currently, we only support element tracking with `scrollInfo`, though in
13103
- * the future we can also offer ViewTimeline support.
13104
- */
13105
- function isElementTracking(options) {
13106
- return options && (options.target || options.offset);
13107
- }
13108
-
13109
- /**
13110
- * If the onScroll function has two arguments, it's expecting
13111
- * more specific information about the scroll from scrollInfo.
13112
- */
13113
- function isOnScrollWithInfo(onScroll) {
13114
- return onScroll.length === 2;
13115
- }
13116
- function attachToFunction(onScroll, options) {
13117
- if (isOnScrollWithInfo(onScroll) || isElementTracking(options)) {
13118
- return scrollInfo((info) => {
13119
- onScroll(info[options.axis].progress, info);
13120
- }, options);
13121
- }
13122
- else {
13123
- return observeTimeline(onScroll, getTimeline(options));
13124
- }
13125
- }
13126
-
13127
- function scroll(onScroll, { axis = "y", container = document.scrollingElement, ...options } = {}) {
13134
+ function scroll(onScroll, { axis = "y", source, container = document.scrollingElement, ...options } = {}) {
13128
13135
  if (!container)
13129
13136
  return noop;
13130
- const optionsWithDefaults = { axis, container, ...options };
13137
+ const optionsWithDefaults = {
13138
+ axis,
13139
+ container: source || container,
13140
+ ...options,
13141
+ };
13131
13142
  return typeof onScroll === "function"
13132
- ? attachToFunction(onScroll, optionsWithDefaults)
13143
+ ? scrollInfo((info) => onScroll(info[axis].progress, info), optionsWithDefaults)
13133
13144
  : attachToAnimation(onScroll, optionsWithDefaults);
13134
13145
  }
13135
13146