framer-motion 12.12.0 → 12.12.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.
@@ -83,520 +83,145 @@
83
83
  const PresenceContext =
84
84
  /* @__PURE__ */ React$1.createContext(null);
85
85
 
86
+ function addUniqueItem(arr, item) {
87
+ if (arr.indexOf(item) === -1)
88
+ arr.push(item);
89
+ }
90
+ function removeItem(arr, item) {
91
+ const index = arr.indexOf(item);
92
+ if (index > -1)
93
+ arr.splice(index, 1);
94
+ }
95
+ // Adapted from array-move
96
+ function moveItem([...arr], fromIndex, toIndex) {
97
+ const startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex;
98
+ if (startIndex >= 0 && startIndex < arr.length) {
99
+ const endIndex = toIndex < 0 ? arr.length + toIndex : toIndex;
100
+ const [item] = arr.splice(fromIndex, 1);
101
+ arr.splice(endIndex, 0, item);
102
+ }
103
+ return arr;
104
+ }
105
+
106
+ const clamp = (min, max, v) => {
107
+ if (v > max)
108
+ return max;
109
+ if (v < min)
110
+ return min;
111
+ return v;
112
+ };
113
+
114
+ exports.warning = () => { };
115
+ exports.invariant = () => { };
116
+ {
117
+ exports.warning = (check, message) => {
118
+ if (!check && typeof console !== "undefined") {
119
+ console.warn(message);
120
+ }
121
+ };
122
+ exports.invariant = (check, message) => {
123
+ if (!check) {
124
+ throw new Error(message);
125
+ }
126
+ };
127
+ }
128
+
129
+ const MotionGlobalConfig = {};
130
+
86
131
  /**
87
- * @public
132
+ * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
88
133
  */
89
- const MotionConfigContext = React$1.createContext({
90
- transformPagePoint: (p) => p,
91
- isStatic: false,
92
- reducedMotion: "never",
93
- });
134
+ const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v);
135
+
136
+ function isObject(value) {
137
+ return typeof value === "object" && value !== null;
138
+ }
94
139
 
95
140
  /**
96
- * Measurement functionality has to be within a separate component
97
- * to leverage snapshot lifecycle.
141
+ * Check if the value is a zero value string like "0px" or "0%"
98
142
  */
99
- class PopChildMeasure extends React__namespace.Component {
100
- getSnapshotBeforeUpdate(prevProps) {
101
- const element = this.props.childRef.current;
102
- if (element && prevProps.isPresent && !this.props.isPresent) {
103
- const parent = element.offsetParent;
104
- const parentWidth = parent instanceof HTMLElement ? parent.offsetWidth || 0 : 0;
105
- const size = this.props.sizeRef.current;
106
- size.height = element.offsetHeight || 0;
107
- size.width = element.offsetWidth || 0;
108
- size.top = element.offsetTop;
109
- size.left = element.offsetLeft;
110
- size.right = parentWidth - size.width - size.left;
111
- }
112
- return null;
143
+ const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v);
144
+
145
+ /*#__NO_SIDE_EFFECTS__*/
146
+ function memo(callback) {
147
+ let result;
148
+ return () => {
149
+ if (result === undefined)
150
+ result = callback();
151
+ return result;
152
+ };
153
+ }
154
+
155
+ /*#__NO_SIDE_EFFECTS__*/
156
+ const noop = (any) => any;
157
+
158
+ /**
159
+ * Pipe
160
+ * Compose other transformers to run linearily
161
+ * pipe(min(20), max(40))
162
+ * @param {...functions} transformers
163
+ * @return {function}
164
+ */
165
+ const combineFunctions = (a, b) => (v) => b(a(v));
166
+ const pipe = (...transformers) => transformers.reduce(combineFunctions);
167
+
168
+ /*
169
+ Progress within given range
170
+
171
+ Given a lower limit and an upper limit, we return the progress
172
+ (expressed as a number 0-1) represented by the given value, and
173
+ limit that progress to within 0-1.
174
+
175
+ @param [number]: Lower limit
176
+ @param [number]: Upper limit
177
+ @param [number]: Value to find progress within given range
178
+ @return [number]: Progress of value within range as expressed 0-1
179
+ */
180
+ /*#__NO_SIDE_EFFECTS__*/
181
+ const progress = (from, to, value) => {
182
+ const toFromDifference = to - from;
183
+ return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
184
+ };
185
+
186
+ class SubscriptionManager {
187
+ constructor() {
188
+ this.subscriptions = [];
113
189
  }
114
- /**
115
- * Required with getSnapshotBeforeUpdate to stop React complaining.
116
- */
117
- componentDidUpdate() { }
118
- render() {
119
- return this.props.children;
190
+ add(handler) {
191
+ addUniqueItem(this.subscriptions, handler);
192
+ return () => removeItem(this.subscriptions, handler);
120
193
  }
121
- }
122
- function PopChild({ children, isPresent, anchorX }) {
123
- const id = React$1.useId();
124
- const ref = React$1.useRef(null);
125
- const size = React$1.useRef({
126
- width: 0,
127
- height: 0,
128
- top: 0,
129
- left: 0,
130
- right: 0,
131
- });
132
- const { nonce } = React$1.useContext(MotionConfigContext);
133
- /**
134
- * We create and inject a style block so we can apply this explicit
135
- * sizing in a non-destructive manner by just deleting the style block.
136
- *
137
- * We can't apply size via render as the measurement happens
138
- * in getSnapshotBeforeUpdate (post-render), likewise if we apply the
139
- * styles directly on the DOM node, we might be overwriting
140
- * styles set via the style prop.
141
- */
142
- React$1.useInsertionEffect(() => {
143
- const { width, height, top, left, right } = size.current;
144
- if (isPresent || !ref.current || !width || !height)
194
+ notify(a, b, c) {
195
+ const numSubscriptions = this.subscriptions.length;
196
+ if (!numSubscriptions)
145
197
  return;
146
- const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`;
147
- ref.current.dataset.motionPopId = id;
148
- const style = document.createElement("style");
149
- if (nonce)
150
- style.nonce = nonce;
151
- document.head.appendChild(style);
152
- if (style.sheet) {
153
- style.sheet.insertRule(`
154
- [data-motion-pop-id="${id}"] {
155
- position: absolute !important;
156
- width: ${width}px !important;
157
- height: ${height}px !important;
158
- ${x}px !important;
159
- top: ${top}px !important;
160
- }
161
- `);
198
+ if (numSubscriptions === 1) {
199
+ /**
200
+ * If there's only a single handler we can just call it without invoking a loop.
201
+ */
202
+ this.subscriptions[0](a, b, c);
162
203
  }
163
- return () => {
164
- if (document.head.contains(style)) {
165
- document.head.removeChild(style);
204
+ else {
205
+ for (let i = 0; i < numSubscriptions; i++) {
206
+ /**
207
+ * Check whether the handler exists before firing as it's possible
208
+ * the subscriptions were modified during this loop running.
209
+ */
210
+ const handler = this.subscriptions[i];
211
+ handler && handler(a, b, c);
166
212
  }
167
- };
168
- }, [isPresent]);
169
- return (jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React__namespace.cloneElement(children, { ref }) }));
170
- }
171
-
172
- const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, }) => {
173
- const presenceChildren = useConstant(newChildrenMap);
174
- const id = React$1.useId();
175
- let isReusedContext = true;
176
- let context = React$1.useMemo(() => {
177
- isReusedContext = false;
178
- return {
179
- id,
180
- initial,
181
- isPresent,
182
- custom,
183
- onExitComplete: (childId) => {
184
- presenceChildren.set(childId, true);
185
- for (const isComplete of presenceChildren.values()) {
186
- if (!isComplete)
187
- return; // can stop searching when any is incomplete
188
- }
189
- onExitComplete && onExitComplete();
190
- },
191
- register: (childId) => {
192
- presenceChildren.set(childId, false);
193
- return () => presenceChildren.delete(childId);
194
- },
195
- };
196
- }, [isPresent, presenceChildren, onExitComplete]);
197
- /**
198
- * If the presence of a child affects the layout of the components around it,
199
- * we want to make a new context value to ensure they get re-rendered
200
- * so they can detect that layout change.
201
- */
202
- if (presenceAffectsLayout && isReusedContext) {
203
- context = { ...context };
213
+ }
204
214
  }
205
- React$1.useMemo(() => {
206
- presenceChildren.forEach((_, key) => presenceChildren.set(key, false));
207
- }, [isPresent]);
208
- /**
209
- * If there's no `motion` components to fire exit animations, we want to remove this
210
- * component immediately.
211
- */
212
- React__namespace.useEffect(() => {
213
- !isPresent &&
214
- !presenceChildren.size &&
215
- onExitComplete &&
216
- onExitComplete();
217
- }, [isPresent]);
218
- if (mode === "popLayout") {
219
- children = (jsx(PopChild, { isPresent: isPresent, anchorX: anchorX, children: children }));
215
+ getSize() {
216
+ return this.subscriptions.length;
217
+ }
218
+ clear() {
219
+ this.subscriptions.length = 0;
220
220
  }
221
- return (jsx(PresenceContext.Provider, { value: context, children: children }));
222
- };
223
- function newChildrenMap() {
224
- return new Map();
225
221
  }
226
222
 
227
223
  /**
228
- * When a component is the child of `AnimatePresence`, it can use `usePresence`
229
- * to access information about whether it's still present in the React tree.
230
- *
231
- * ```jsx
232
- * import { usePresence } from "framer-motion"
233
- *
234
- * export const Component = () => {
235
- * const [isPresent, safeToRemove] = usePresence()
236
- *
237
- * useEffect(() => {
238
- * !isPresent && setTimeout(safeToRemove, 1000)
239
- * }, [isPresent])
240
- *
241
- * return <div />
242
- * }
243
- * ```
244
- *
245
- * If `isPresent` is `false`, it means that a component has been removed the tree, but
246
- * `AnimatePresence` won't really remove it until `safeToRemove` has been called.
247
- *
248
- * @public
249
- */
250
- function usePresence(subscribe = true) {
251
- const context = React$1.useContext(PresenceContext);
252
- if (context === null)
253
- return [true, null];
254
- const { isPresent, onExitComplete, register } = context;
255
- // It's safe to call the following hooks conditionally (after an early return) because the context will always
256
- // either be null or non-null for the lifespan of the component.
257
- const id = React$1.useId();
258
- React$1.useEffect(() => {
259
- if (subscribe) {
260
- return register(id);
261
- }
262
- }, [subscribe]);
263
- const safeToRemove = React$1.useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
264
- return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
265
- }
266
- /**
267
- * Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
268
- * There is no `safeToRemove` function.
269
- *
270
- * ```jsx
271
- * import { useIsPresent } from "framer-motion"
272
- *
273
- * export const Component = () => {
274
- * const isPresent = useIsPresent()
275
- *
276
- * useEffect(() => {
277
- * !isPresent && console.log("I've been removed!")
278
- * }, [isPresent])
279
- *
280
- * return <div />
281
- * }
282
- * ```
283
- *
284
- * @public
285
- */
286
- function useIsPresent() {
287
- return isPresent(React$1.useContext(PresenceContext));
288
- }
289
- function isPresent(context) {
290
- return context === null ? true : context.isPresent;
291
- }
292
-
293
- const getChildKey = (child) => child.key || "";
294
- function onlyElements(children) {
295
- const filtered = [];
296
- // We use forEach here instead of map as map mutates the component key by preprending `.$`
297
- React$1.Children.forEach(children, (child) => {
298
- if (React$1.isValidElement(child))
299
- filtered.push(child);
300
- });
301
- return filtered;
302
- }
303
-
304
- /**
305
- * `AnimatePresence` enables the animation of components that have been removed from the tree.
306
- *
307
- * When adding/removing more than a single child, every child **must** be given a unique `key` prop.
308
- *
309
- * Any `motion` components that have an `exit` property defined will animate out when removed from
310
- * the tree.
311
- *
312
- * ```jsx
313
- * import { motion, AnimatePresence } from 'framer-motion'
314
- *
315
- * export const Items = ({ items }) => (
316
- * <AnimatePresence>
317
- * {items.map(item => (
318
- * <motion.div
319
- * key={item.id}
320
- * initial={{ opacity: 0 }}
321
- * animate={{ opacity: 1 }}
322
- * exit={{ opacity: 0 }}
323
- * />
324
- * ))}
325
- * </AnimatePresence>
326
- * )
327
- * ```
328
- *
329
- * You can sequence exit animations throughout a tree using variants.
330
- *
331
- * If a child contains multiple `motion` components with `exit` props, it will only unmount the child
332
- * once all `motion` components have finished animating out. Likewise, any components using
333
- * `usePresence` all need to call `safeToRemove`.
334
- *
335
- * @public
336
- */
337
- const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", }) => {
338
- const [isParentPresent, safeToRemove] = usePresence(propagate);
339
- /**
340
- * Filter any children that aren't ReactElements. We can only track components
341
- * between renders with a props.key.
342
- */
343
- const presentChildren = React$1.useMemo(() => onlyElements(children), [children]);
344
- /**
345
- * Track the keys of the currently rendered children. This is used to
346
- * determine which children are exiting.
347
- */
348
- const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
349
- /**
350
- * If `initial={false}` we only want to pass this to components in the first render.
351
- */
352
- const isInitialRender = React$1.useRef(true);
353
- /**
354
- * A ref containing the currently present children. When all exit animations
355
- * are complete, we use this to re-render the component with the latest children
356
- * *committed* rather than the latest children *rendered*.
357
- */
358
- const pendingPresentChildren = React$1.useRef(presentChildren);
359
- /**
360
- * Track which exiting children have finished animating out.
361
- */
362
- const exitComplete = useConstant(() => new Map());
363
- /**
364
- * Save children to render as React state. To ensure this component is concurrent-safe,
365
- * we check for exiting children via an effect.
366
- */
367
- const [diffedChildren, setDiffedChildren] = React$1.useState(presentChildren);
368
- const [renderedChildren, setRenderedChildren] = React$1.useState(presentChildren);
369
- useIsomorphicLayoutEffect(() => {
370
- isInitialRender.current = false;
371
- pendingPresentChildren.current = presentChildren;
372
- /**
373
- * Update complete status of exiting children.
374
- */
375
- for (let i = 0; i < renderedChildren.length; i++) {
376
- const key = getChildKey(renderedChildren[i]);
377
- if (!presentKeys.includes(key)) {
378
- if (exitComplete.get(key) !== true) {
379
- exitComplete.set(key, false);
380
- }
381
- }
382
- else {
383
- exitComplete.delete(key);
384
- }
385
- }
386
- }, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
387
- const exitingChildren = [];
388
- if (presentChildren !== diffedChildren) {
389
- let nextChildren = [...presentChildren];
390
- /**
391
- * Loop through all the currently rendered components and decide which
392
- * are exiting.
393
- */
394
- for (let i = 0; i < renderedChildren.length; i++) {
395
- const child = renderedChildren[i];
396
- const key = getChildKey(child);
397
- if (!presentKeys.includes(key)) {
398
- nextChildren.splice(i, 0, child);
399
- exitingChildren.push(child);
400
- }
401
- }
402
- /**
403
- * If we're in "wait" mode, and we have exiting children, we want to
404
- * only render these until they've all exited.
405
- */
406
- if (mode === "wait" && exitingChildren.length) {
407
- nextChildren = exitingChildren;
408
- }
409
- setRenderedChildren(onlyElements(nextChildren));
410
- setDiffedChildren(presentChildren);
411
- /**
412
- * Early return to ensure once we've set state with the latest diffed
413
- * children, we can immediately re-render.
414
- */
415
- return null;
416
- }
417
- if (mode === "wait" &&
418
- renderedChildren.length > 1) {
419
- console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`);
420
- }
421
- /**
422
- * If we've been provided a forceRender function by the LayoutGroupContext,
423
- * we can use it to force a re-render amongst all surrounding components once
424
- * all components have finished animating out.
425
- */
426
- const { forceRender } = React$1.useContext(LayoutGroupContext);
427
- return (jsx(Fragment, { children: renderedChildren.map((child) => {
428
- const key = getChildKey(child);
429
- const isPresent = propagate && !isParentPresent
430
- ? false
431
- : presentChildren === renderedChildren ||
432
- presentKeys.includes(key);
433
- const onExit = () => {
434
- if (exitComplete.has(key)) {
435
- exitComplete.set(key, true);
436
- }
437
- else {
438
- return;
439
- }
440
- let isEveryExitComplete = true;
441
- exitComplete.forEach((isExitComplete) => {
442
- if (!isExitComplete)
443
- isEveryExitComplete = false;
444
- });
445
- if (isEveryExitComplete) {
446
- forceRender?.();
447
- setRenderedChildren(pendingPresentChildren.current);
448
- propagate && safeToRemove?.();
449
- onExitComplete && onExitComplete();
450
- }
451
- };
452
- return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
453
- ? undefined
454
- : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
455
- }) }));
456
- };
457
-
458
- /**
459
- * Note: Still used by components generated by old versions of Framer
460
- *
461
- * @deprecated
462
- */
463
- const DeprecatedLayoutGroupContext = React$1.createContext(null);
464
-
465
- function addUniqueItem(arr, item) {
466
- if (arr.indexOf(item) === -1)
467
- arr.push(item);
468
- }
469
- function removeItem(arr, item) {
470
- const index = arr.indexOf(item);
471
- if (index > -1)
472
- arr.splice(index, 1);
473
- }
474
- // Adapted from array-move
475
- function moveItem([...arr], fromIndex, toIndex) {
476
- const startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex;
477
- if (startIndex >= 0 && startIndex < arr.length) {
478
- const endIndex = toIndex < 0 ? arr.length + toIndex : toIndex;
479
- const [item] = arr.splice(fromIndex, 1);
480
- arr.splice(endIndex, 0, item);
481
- }
482
- return arr;
483
- }
484
-
485
- const clamp = (min, max, v) => {
486
- if (v > max)
487
- return max;
488
- if (v < min)
489
- return min;
490
- return v;
491
- };
492
-
493
- exports.warning = () => { };
494
- exports.invariant = () => { };
495
- {
496
- exports.warning = (check, message) => {
497
- if (!check && typeof console !== "undefined") {
498
- console.warn(message);
499
- }
500
- };
501
- exports.invariant = (check, message) => {
502
- if (!check) {
503
- throw new Error(message);
504
- }
505
- };
506
- }
507
-
508
- const MotionGlobalConfig = {};
509
-
510
- /**
511
- * Check if value is a numerical string, ie a string that is purely a number eg "100" or "-100.1"
512
- */
513
- const isNumericalString = (v) => /^-?(?:\d+(?:\.\d+)?|\.\d+)$/u.test(v);
514
-
515
- /**
516
- * Check if the value is a zero value string like "0px" or "0%"
517
- */
518
- const isZeroValueString = (v) => /^0[^.\s]+$/u.test(v);
519
-
520
- /*#__NO_SIDE_EFFECTS__*/
521
- function memo(callback) {
522
- let result;
523
- return () => {
524
- if (result === undefined)
525
- result = callback();
526
- return result;
527
- };
528
- }
529
-
530
- /*#__NO_SIDE_EFFECTS__*/
531
- const noop = (any) => any;
532
-
533
- /**
534
- * Pipe
535
- * Compose other transformers to run linearily
536
- * pipe(min(20), max(40))
537
- * @param {...functions} transformers
538
- * @return {function}
539
- */
540
- const combineFunctions = (a, b) => (v) => b(a(v));
541
- const pipe = (...transformers) => transformers.reduce(combineFunctions);
542
-
543
- /*
544
- Progress within given range
545
-
546
- Given a lower limit and an upper limit, we return the progress
547
- (expressed as a number 0-1) represented by the given value, and
548
- limit that progress to within 0-1.
549
-
550
- @param [number]: Lower limit
551
- @param [number]: Upper limit
552
- @param [number]: Value to find progress within given range
553
- @return [number]: Progress of value within range as expressed 0-1
554
- */
555
- /*#__NO_SIDE_EFFECTS__*/
556
- const progress = (from, to, value) => {
557
- const toFromDifference = to - from;
558
- return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
559
- };
560
-
561
- class SubscriptionManager {
562
- constructor() {
563
- this.subscriptions = [];
564
- }
565
- add(handler) {
566
- addUniqueItem(this.subscriptions, handler);
567
- return () => removeItem(this.subscriptions, handler);
568
- }
569
- notify(a, b, c) {
570
- const numSubscriptions = this.subscriptions.length;
571
- if (!numSubscriptions)
572
- return;
573
- if (numSubscriptions === 1) {
574
- /**
575
- * If there's only a single handler we can just call it without invoking a loop.
576
- */
577
- this.subscriptions[0](a, b, c);
578
- }
579
- else {
580
- for (let i = 0; i < numSubscriptions; i++) {
581
- /**
582
- * Check whether the handler exists before firing as it's possible
583
- * the subscriptions were modified during this loop running.
584
- */
585
- const handler = this.subscriptions[i];
586
- handler && handler(a, b, c);
587
- }
588
- }
589
- }
590
- getSize() {
591
- return this.subscriptions.length;
592
- }
593
- clear() {
594
- this.subscriptions.length = 0;
595
- }
596
- }
597
-
598
- /**
599
- * Converts seconds to milliseconds
224
+ * Converts seconds to milliseconds
600
225
  *
601
226
  * @param seconds - Time in seconds.
602
227
  * @return milliseconds - Converted time in milliseconds.
@@ -3055,6 +2680,14 @@
3055
2680
  ((type === "spring" || isGenerator(type)) && velocity));
3056
2681
  }
3057
2682
 
2683
+ /**
2684
+ * Checks if an element is an HTML element in a way
2685
+ * that works across iframes
2686
+ */
2687
+ function isHTMLElement(element) {
2688
+ return isObject(element) && "offsetHeight" in element;
2689
+ }
2690
+
3058
2691
  /**
3059
2692
  * A list of values that can be hardware-accelerated.
3060
2693
  */
@@ -3063,16 +2696,13 @@
3063
2696
  "clipPath",
3064
2697
  "filter",
3065
2698
  "transform",
3066
- // TODO: Can be accelerated but currently disabled until https://issues.chromium.org/issues/41491098 is resolved
3067
- // or until we implement support for linear() easing.
2699
+ // TODO: Could be re-enabled now we have support for linear() easing
3068
2700
  // "background-color"
3069
2701
  ]);
3070
2702
  const supportsWaapi = /*@__PURE__*/ memo(() => Object.hasOwnProperty.call(Element.prototype, "animate"));
3071
2703
  function supportsBrowserAnimation(options) {
3072
2704
  const { motionValue, name, repeatDelay, repeatType, damping, type } = options;
3073
- if (!motionValue ||
3074
- !motionValue.owner ||
3075
- !(motionValue.owner.current instanceof HTMLElement)) {
2705
+ if (!isHTMLElement(motionValue?.owner?.current)) {
3076
2706
  return false;
3077
2707
  }
3078
2708
  const { onUpdate, transformTemplate } = motionValue.owner.getProps();
@@ -4101,1035 +3731,1432 @@
4101
3731
  this.events.animationCancel.notify();
4102
3732
  }
4103
3733
  }
4104
- this.clearAnimation();
4105
- }
4106
- /**
4107
- * Returns `true` if this value is currently animating.
4108
- *
4109
- * @public
4110
- */
4111
- isAnimating() {
4112
- return !!this.animation;
4113
- }
4114
- clearAnimation() {
4115
- delete this.animation;
3734
+ this.clearAnimation();
3735
+ }
3736
+ /**
3737
+ * Returns `true` if this value is currently animating.
3738
+ *
3739
+ * @public
3740
+ */
3741
+ isAnimating() {
3742
+ return !!this.animation;
3743
+ }
3744
+ clearAnimation() {
3745
+ delete this.animation;
3746
+ }
3747
+ /**
3748
+ * Destroy and clean up subscribers to this `MotionValue`.
3749
+ *
3750
+ * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
3751
+ * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
3752
+ * created a `MotionValue` via the `motionValue` function.
3753
+ *
3754
+ * @public
3755
+ */
3756
+ destroy() {
3757
+ this.dependents?.clear();
3758
+ this.events.destroy?.notify();
3759
+ this.clearListeners();
3760
+ this.stop();
3761
+ if (this.stopPassiveEffect) {
3762
+ this.stopPassiveEffect();
3763
+ }
3764
+ }
3765
+ }
3766
+ function motionValue(init, options) {
3767
+ return new MotionValue(init, options);
3768
+ }
3769
+
3770
+ /**
3771
+ * Provided a value and a ValueType, returns the value as that value type.
3772
+ */
3773
+ const getValueAsType = (value, type) => {
3774
+ return type && typeof value === "number"
3775
+ ? type.transform(value)
3776
+ : value;
3777
+ };
3778
+
3779
+ class MotionValueState {
3780
+ constructor() {
3781
+ this.latest = {};
3782
+ this.values = new Map();
3783
+ }
3784
+ set(name, value, render, computed) {
3785
+ const existingValue = this.values.get(name);
3786
+ if (existingValue) {
3787
+ existingValue.onRemove();
3788
+ }
3789
+ const onChange = () => {
3790
+ this.latest[name] = getValueAsType(value.get(), numberValueTypes[name]);
3791
+ render && frame.render(render);
3792
+ };
3793
+ onChange();
3794
+ const cancelOnChange = value.on("change", onChange);
3795
+ computed && value.addDependent(computed);
3796
+ const remove = () => {
3797
+ cancelOnChange();
3798
+ render && cancelFrame(render);
3799
+ this.values.delete(name);
3800
+ computed && value.removeDependent(computed);
3801
+ };
3802
+ this.values.set(name, { value, onRemove: remove });
3803
+ return remove;
3804
+ }
3805
+ get(name) {
3806
+ return this.values.get(name)?.value;
3807
+ }
3808
+ destroy() {
3809
+ for (const value of this.values.values()) {
3810
+ value.onRemove();
3811
+ }
3812
+ }
3813
+ }
3814
+
3815
+ const translateAlias$1 = {
3816
+ x: "translateX",
3817
+ y: "translateY",
3818
+ z: "translateZ",
3819
+ transformPerspective: "perspective",
3820
+ };
3821
+ function buildTransform$1(state) {
3822
+ let transform = "";
3823
+ let transformIsDefault = true;
3824
+ /**
3825
+ * Loop over all possible transforms in order, adding the ones that
3826
+ * are present to the transform string.
3827
+ */
3828
+ for (let i = 0; i < transformPropOrder.length; i++) {
3829
+ const key = transformPropOrder[i];
3830
+ const value = state.latest[key];
3831
+ if (value === undefined)
3832
+ continue;
3833
+ let valueIsDefault = true;
3834
+ if (typeof value === "number") {
3835
+ valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
3836
+ }
3837
+ else {
3838
+ valueIsDefault = parseFloat(value) === 0;
3839
+ }
3840
+ if (!valueIsDefault) {
3841
+ transformIsDefault = false;
3842
+ const transformName = translateAlias$1[key] || key;
3843
+ const valueToRender = state.latest[key];
3844
+ transform += `${transformName}(${valueToRender}) `;
3845
+ }
3846
+ }
3847
+ return transformIsDefault ? "none" : transform.trim();
3848
+ }
3849
+
3850
+ const stateMap = new WeakMap();
3851
+ function styleEffect(subject, values) {
3852
+ const elements = resolveElements(subject);
3853
+ const subscriptions = [];
3854
+ for (let i = 0; i < elements.length; i++) {
3855
+ const element = elements[i];
3856
+ const state = stateMap.get(element) ?? new MotionValueState();
3857
+ stateMap.set(element, state);
3858
+ for (const key in values) {
3859
+ const value = values[key];
3860
+ const remove = addValue(element, state, key, value);
3861
+ subscriptions.push(remove);
3862
+ }
3863
+ }
3864
+ return () => {
3865
+ for (const cancel of subscriptions)
3866
+ cancel();
3867
+ };
3868
+ }
3869
+ function addValue(element, state, key, value) {
3870
+ let render = undefined;
3871
+ let computed = undefined;
3872
+ if (transformProps.has(key)) {
3873
+ if (!state.get("transform")) {
3874
+ state.set("transform", new MotionValue("none"), () => {
3875
+ element.style.transform = buildTransform$1(state);
3876
+ });
3877
+ }
3878
+ computed = state.get("transform");
3879
+ }
3880
+ else if (isCSSVar(key)) {
3881
+ render = () => {
3882
+ element.style.setProperty(key, state.latest[key]);
3883
+ };
3884
+ }
3885
+ else {
3886
+ render = () => {
3887
+ element.style[key] = state.latest[key];
3888
+ };
3889
+ }
3890
+ return state.set(key, value, render, computed);
3891
+ }
3892
+
3893
+ const { schedule: microtask, cancel: cancelMicrotask } =
3894
+ /* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
3895
+
3896
+ const isDragging = {
3897
+ x: false,
3898
+ y: false,
3899
+ };
3900
+ function isDragActive() {
3901
+ return isDragging.x || isDragging.y;
3902
+ }
3903
+
3904
+ function setDragLock(axis) {
3905
+ if (axis === "x" || axis === "y") {
3906
+ if (isDragging[axis]) {
3907
+ return null;
3908
+ }
3909
+ else {
3910
+ isDragging[axis] = true;
3911
+ return () => {
3912
+ isDragging[axis] = false;
3913
+ };
3914
+ }
4116
3915
  }
4117
- /**
4118
- * Destroy and clean up subscribers to this `MotionValue`.
4119
- *
4120
- * The `MotionValue` hooks like `useMotionValue` and `useTransform` automatically
4121
- * handle the lifecycle of the returned `MotionValue`, so this method is only necessary if you've manually
4122
- * created a `MotionValue` via the `motionValue` function.
4123
- *
4124
- * @public
4125
- */
4126
- destroy() {
4127
- this.dependents?.clear();
4128
- this.events.destroy?.notify();
4129
- this.clearListeners();
4130
- this.stop();
4131
- if (this.stopPassiveEffect) {
4132
- this.stopPassiveEffect();
3916
+ else {
3917
+ if (isDragging.x || isDragging.y) {
3918
+ return null;
3919
+ }
3920
+ else {
3921
+ isDragging.x = isDragging.y = true;
3922
+ return () => {
3923
+ isDragging.x = isDragging.y = false;
3924
+ };
4133
3925
  }
4134
3926
  }
4135
3927
  }
4136
- function motionValue(init, options) {
4137
- return new MotionValue(init, options);
3928
+
3929
+ function setupGesture(elementOrSelector, options) {
3930
+ const elements = resolveElements(elementOrSelector);
3931
+ const gestureAbortController = new AbortController();
3932
+ const eventOptions = {
3933
+ passive: true,
3934
+ ...options,
3935
+ signal: gestureAbortController.signal,
3936
+ };
3937
+ const cancel = () => gestureAbortController.abort();
3938
+ return [elements, eventOptions, cancel];
4138
3939
  }
4139
3940
 
3941
+ function isValidHover(event) {
3942
+ return !(event.pointerType === "touch" || isDragActive());
3943
+ }
4140
3944
  /**
4141
- * Provided a value and a ValueType, returns the value as that value type.
3945
+ * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
3946
+ * in that it has an easier syntax, filters out polyfilled touch events, interoperates
3947
+ * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
3948
+ *
3949
+ * @public
4142
3950
  */
4143
- const getValueAsType = (value, type) => {
4144
- return type && typeof value === "number"
4145
- ? type.transform(value)
4146
- : value;
3951
+ function hover(elementOrSelector, onHoverStart, options = {}) {
3952
+ const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
3953
+ const onPointerEnter = (enterEvent) => {
3954
+ if (!isValidHover(enterEvent))
3955
+ return;
3956
+ const { target } = enterEvent;
3957
+ const onHoverEnd = onHoverStart(target, enterEvent);
3958
+ if (typeof onHoverEnd !== "function" || !target)
3959
+ return;
3960
+ const onPointerLeave = (leaveEvent) => {
3961
+ if (!isValidHover(leaveEvent))
3962
+ return;
3963
+ onHoverEnd(leaveEvent);
3964
+ target.removeEventListener("pointerleave", onPointerLeave);
3965
+ };
3966
+ target.addEventListener("pointerleave", onPointerLeave, eventOptions);
3967
+ };
3968
+ elements.forEach((element) => {
3969
+ element.addEventListener("pointerenter", onPointerEnter, eventOptions);
3970
+ });
3971
+ return cancel;
3972
+ }
3973
+
3974
+ /**
3975
+ * Recursively traverse up the tree to check whether the provided child node
3976
+ * is the parent or a descendant of it.
3977
+ *
3978
+ * @param parent - Element to find
3979
+ * @param child - Element to test against parent
3980
+ */
3981
+ const isNodeOrChild = (parent, child) => {
3982
+ if (!child) {
3983
+ return false;
3984
+ }
3985
+ else if (parent === child) {
3986
+ return true;
3987
+ }
3988
+ else {
3989
+ return isNodeOrChild(parent, child.parentElement);
3990
+ }
4147
3991
  };
4148
3992
 
4149
- class MotionValueState {
4150
- constructor() {
4151
- this.latest = {};
4152
- this.values = new Map();
3993
+ const isPrimaryPointer = (event) => {
3994
+ if (event.pointerType === "mouse") {
3995
+ return typeof event.button !== "number" || event.button <= 0;
4153
3996
  }
4154
- set(name, value, render, computed) {
4155
- const existingValue = this.values.get(name);
4156
- if (existingValue) {
4157
- existingValue.onRemove();
4158
- }
4159
- const onChange = () => {
4160
- this.latest[name] = getValueAsType(value.get(), numberValueTypes[name]);
4161
- render && frame.render(render);
3997
+ else {
3998
+ /**
3999
+ * isPrimary is true for all mice buttons, whereas every touch point
4000
+ * is regarded as its own input. So subsequent concurrent touch points
4001
+ * will be false.
4002
+ *
4003
+ * Specifically match against false here as incomplete versions of
4004
+ * PointerEvents in very old browser might have it set as undefined.
4005
+ */
4006
+ return event.isPrimary !== false;
4007
+ }
4008
+ };
4009
+
4010
+ const focusableElements = new Set([
4011
+ "BUTTON",
4012
+ "INPUT",
4013
+ "SELECT",
4014
+ "TEXTAREA",
4015
+ "A",
4016
+ ]);
4017
+ function isElementKeyboardAccessible(element) {
4018
+ return (focusableElements.has(element.tagName) ||
4019
+ element.tabIndex !== -1);
4020
+ }
4021
+
4022
+ const isPressing = new WeakSet();
4023
+
4024
+ /**
4025
+ * Filter out events that are not "Enter" keys.
4026
+ */
4027
+ function filterEvents(callback) {
4028
+ return (event) => {
4029
+ if (event.key !== "Enter")
4030
+ return;
4031
+ callback(event);
4032
+ };
4033
+ }
4034
+ function firePointerEvent(target, type) {
4035
+ target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
4036
+ }
4037
+ const enableKeyboardPress = (focusEvent, eventOptions) => {
4038
+ const element = focusEvent.currentTarget;
4039
+ if (!element)
4040
+ return;
4041
+ const handleKeydown = filterEvents(() => {
4042
+ if (isPressing.has(element))
4043
+ return;
4044
+ firePointerEvent(element, "down");
4045
+ const handleKeyup = filterEvents(() => {
4046
+ firePointerEvent(element, "up");
4047
+ });
4048
+ const handleBlur = () => firePointerEvent(element, "cancel");
4049
+ element.addEventListener("keyup", handleKeyup, eventOptions);
4050
+ element.addEventListener("blur", handleBlur, eventOptions);
4051
+ });
4052
+ element.addEventListener("keydown", handleKeydown, eventOptions);
4053
+ /**
4054
+ * Add an event listener that fires on blur to remove the keydown events.
4055
+ */
4056
+ element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
4057
+ };
4058
+
4059
+ /**
4060
+ * Filter out events that are not primary pointer events, or are triggering
4061
+ * while a Motion gesture is active.
4062
+ */
4063
+ function isValidPressEvent(event) {
4064
+ return isPrimaryPointer(event) && !isDragActive();
4065
+ }
4066
+ /**
4067
+ * Create a press gesture.
4068
+ *
4069
+ * Press is different to `"pointerdown"`, `"pointerup"` in that it
4070
+ * automatically filters out secondary pointer events like right
4071
+ * click and multitouch.
4072
+ *
4073
+ * It also adds accessibility support for keyboards, where
4074
+ * an element with a press gesture will receive focus and
4075
+ * trigger on Enter `"keydown"` and `"keyup"` events.
4076
+ *
4077
+ * This is different to a browser's `"click"` event, which does
4078
+ * respond to keyboards but only for the `"click"` itself, rather
4079
+ * than the press start and end/cancel. The element also needs
4080
+ * to be focusable for this to work, whereas a press gesture will
4081
+ * make an element focusable by default.
4082
+ *
4083
+ * @public
4084
+ */
4085
+ function press(targetOrSelector, onPressStart, options = {}) {
4086
+ const [targets, eventOptions, cancelEvents] = setupGesture(targetOrSelector, options);
4087
+ const startPress = (startEvent) => {
4088
+ const target = startEvent.currentTarget;
4089
+ if (!isValidPressEvent(startEvent))
4090
+ return;
4091
+ isPressing.add(target);
4092
+ const onPressEnd = onPressStart(target, startEvent);
4093
+ const onPointerEnd = (endEvent, success) => {
4094
+ window.removeEventListener("pointerup", onPointerUp);
4095
+ window.removeEventListener("pointercancel", onPointerCancel);
4096
+ if (isPressing.has(target)) {
4097
+ isPressing.delete(target);
4098
+ }
4099
+ if (!isValidPressEvent(endEvent)) {
4100
+ return;
4101
+ }
4102
+ if (typeof onPressEnd === "function") {
4103
+ onPressEnd(endEvent, { success });
4104
+ }
4162
4105
  };
4163
- onChange();
4164
- const cancelOnChange = value.on("change", onChange);
4165
- computed && value.addDependent(computed);
4166
- const remove = () => {
4167
- cancelOnChange();
4168
- render && cancelFrame(render);
4169
- this.values.delete(name);
4170
- computed && value.removeDependent(computed);
4106
+ const onPointerUp = (upEvent) => {
4107
+ onPointerEnd(upEvent, target === window ||
4108
+ target === document ||
4109
+ options.useGlobalTarget ||
4110
+ isNodeOrChild(target, upEvent.target));
4171
4111
  };
4172
- this.values.set(name, { value, onRemove: remove });
4173
- return remove;
4174
- }
4175
- get(name) {
4176
- return this.values.get(name)?.value;
4177
- }
4178
- destroy() {
4179
- for (const value of this.values.values()) {
4180
- value.onRemove();
4112
+ const onPointerCancel = (cancelEvent) => {
4113
+ onPointerEnd(cancelEvent, false);
4114
+ };
4115
+ window.addEventListener("pointerup", onPointerUp, eventOptions);
4116
+ window.addEventListener("pointercancel", onPointerCancel, eventOptions);
4117
+ };
4118
+ targets.forEach((target) => {
4119
+ const pointerDownTarget = options.useGlobalTarget ? window : target;
4120
+ pointerDownTarget.addEventListener("pointerdown", startPress, eventOptions);
4121
+ if (isHTMLElement(target)) {
4122
+ target.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions));
4123
+ if (!isElementKeyboardAccessible(target) &&
4124
+ !target.hasAttribute("tabindex")) {
4125
+ target.tabIndex = 0;
4126
+ }
4181
4127
  }
4182
- }
4128
+ });
4129
+ return cancelEvents;
4183
4130
  }
4184
4131
 
4185
- const translateAlias$1 = {
4186
- x: "translateX",
4187
- y: "translateY",
4188
- z: "translateZ",
4189
- transformPerspective: "perspective",
4190
- };
4191
- function buildTransform$1(state) {
4192
- let transform = "";
4193
- let transformIsDefault = true;
4194
- /**
4195
- * Loop over all possible transforms in order, adding the ones that
4196
- * are present to the transform string.
4197
- */
4198
- for (let i = 0; i < transformPropOrder.length; i++) {
4199
- const key = transformPropOrder[i];
4200
- const value = state.latest[key];
4201
- if (value === undefined)
4202
- continue;
4203
- let valueIsDefault = true;
4204
- if (typeof value === "number") {
4205
- valueIsDefault = value === (key.startsWith("scale") ? 1 : 0);
4206
- }
4207
- else {
4208
- valueIsDefault = parseFloat(value) === 0;
4209
- }
4210
- if (!valueIsDefault) {
4211
- transformIsDefault = false;
4212
- const transformName = translateAlias$1[key] || key;
4213
- const valueToRender = state.latest[key];
4214
- transform += `${transformName}(${valueToRender}) `;
4215
- }
4216
- }
4217
- return transformIsDefault ? "none" : transform.trim();
4132
+ function getComputedStyle$2(element, name) {
4133
+ const computedStyle = window.getComputedStyle(element);
4134
+ return isCSSVar(name)
4135
+ ? computedStyle.getPropertyValue(name)
4136
+ : computedStyle[name];
4218
4137
  }
4219
4138
 
4220
- const stateMap = new WeakMap();
4221
- function styleEffect(subject, values) {
4222
- const elements = resolveElements(subject);
4223
- const subscriptions = [];
4224
- for (let i = 0; i < elements.length; i++) {
4225
- const element = elements[i];
4226
- const state = stateMap.get(element) ?? new MotionValueState();
4227
- stateMap.set(element, state);
4228
- for (const key in values) {
4229
- const value = values[key];
4230
- const remove = addValue(element, state, key, value);
4231
- subscriptions.push(remove);
4139
+ function observeTimeline(update, timeline) {
4140
+ let prevProgress;
4141
+ const onFrame = () => {
4142
+ const { currentTime } = timeline;
4143
+ const percentage = currentTime === null ? 0 : currentTime.value;
4144
+ const progress = percentage / 100;
4145
+ if (prevProgress !== progress) {
4146
+ update(progress);
4232
4147
  }
4233
- }
4234
- return () => {
4235
- for (const cancel of subscriptions)
4236
- cancel();
4148
+ prevProgress = progress;
4237
4149
  };
4150
+ frame.preUpdate(onFrame, true);
4151
+ return () => cancelFrame(onFrame);
4238
4152
  }
4239
- function addValue(element, state, key, value) {
4240
- let render = undefined;
4241
- let computed = undefined;
4242
- if (transformProps.has(key)) {
4243
- if (!state.get("transform")) {
4244
- state.set("transform", new MotionValue("none"), () => {
4245
- element.style.transform = buildTransform$1(state);
4246
- });
4247
- }
4248
- computed = state.get("transform");
4249
- }
4250
- else if (isCSSVar(key)) {
4251
- render = () => {
4252
- element.style.setProperty(key, state.latest[key]);
4253
- };
4153
+
4154
+ function record() {
4155
+ const { value } = statsBuffer;
4156
+ if (value === null) {
4157
+ cancelFrame(record);
4158
+ return;
4254
4159
  }
4255
- else {
4256
- render = () => {
4257
- element.style[key] = state.latest[key];
4160
+ value.frameloop.rate.push(frameData.delta);
4161
+ value.animations.mainThread.push(activeAnimations.mainThread);
4162
+ value.animations.waapi.push(activeAnimations.waapi);
4163
+ value.animations.layout.push(activeAnimations.layout);
4164
+ }
4165
+ function mean(values) {
4166
+ return values.reduce((acc, value) => acc + value, 0) / values.length;
4167
+ }
4168
+ function summarise(values, calcAverage = mean) {
4169
+ if (values.length === 0) {
4170
+ return {
4171
+ min: 0,
4172
+ max: 0,
4173
+ avg: 0,
4258
4174
  };
4259
4175
  }
4260
- return state.set(key, value, render, computed);
4176
+ return {
4177
+ min: Math.min(...values),
4178
+ max: Math.max(...values),
4179
+ avg: calcAverage(values),
4180
+ };
4261
4181
  }
4262
-
4263
- const { schedule: microtask, cancel: cancelMicrotask } =
4264
- /* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
4265
-
4266
- const isDragging = {
4267
- x: false,
4268
- y: false,
4269
- };
4270
- function isDragActive() {
4271
- return isDragging.x || isDragging.y;
4182
+ const msToFps = (ms) => Math.round(1000 / ms);
4183
+ function clearStatsBuffer() {
4184
+ statsBuffer.value = null;
4185
+ statsBuffer.addProjectionMetrics = null;
4272
4186
  }
4273
-
4274
- function setDragLock(axis) {
4275
- if (axis === "x" || axis === "y") {
4276
- if (isDragging[axis]) {
4277
- return null;
4278
- }
4279
- else {
4280
- isDragging[axis] = true;
4281
- return () => {
4282
- isDragging[axis] = false;
4283
- };
4284
- }
4285
- }
4286
- else {
4287
- if (isDragging.x || isDragging.y) {
4288
- return null;
4289
- }
4290
- else {
4291
- isDragging.x = isDragging.y = true;
4292
- return () => {
4293
- isDragging.x = isDragging.y = false;
4294
- };
4295
- }
4187
+ function reportStats() {
4188
+ const { value } = statsBuffer;
4189
+ if (!value) {
4190
+ throw new Error("Stats are not being measured");
4296
4191
  }
4297
- }
4298
-
4299
- function setupGesture(elementOrSelector, options) {
4300
- const elements = resolveElements(elementOrSelector);
4301
- const gestureAbortController = new AbortController();
4302
- const eventOptions = {
4303
- passive: true,
4304
- ...options,
4305
- signal: gestureAbortController.signal,
4192
+ clearStatsBuffer();
4193
+ cancelFrame(record);
4194
+ const summary = {
4195
+ frameloop: {
4196
+ setup: summarise(value.frameloop.setup),
4197
+ rate: summarise(value.frameloop.rate),
4198
+ read: summarise(value.frameloop.read),
4199
+ resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
4200
+ preUpdate: summarise(value.frameloop.preUpdate),
4201
+ update: summarise(value.frameloop.update),
4202
+ preRender: summarise(value.frameloop.preRender),
4203
+ render: summarise(value.frameloop.render),
4204
+ postRender: summarise(value.frameloop.postRender),
4205
+ },
4206
+ animations: {
4207
+ mainThread: summarise(value.animations.mainThread),
4208
+ waapi: summarise(value.animations.waapi),
4209
+ layout: summarise(value.animations.layout),
4210
+ },
4211
+ layoutProjection: {
4212
+ nodes: summarise(value.layoutProjection.nodes),
4213
+ calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
4214
+ calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
4215
+ },
4306
4216
  };
4307
- const cancel = () => gestureAbortController.abort();
4308
- return [elements, eventOptions, cancel];
4309
- }
4310
-
4311
- function isValidHover(event) {
4312
- return !(event.pointerType === "touch" || isDragActive());
4217
+ /**
4218
+ * Convert the rate to FPS
4219
+ */
4220
+ const { rate } = summary.frameloop;
4221
+ rate.min = msToFps(rate.min);
4222
+ rate.max = msToFps(rate.max);
4223
+ rate.avg = msToFps(rate.avg);
4224
+ [rate.min, rate.max] = [rate.max, rate.min];
4225
+ return summary;
4313
4226
  }
4314
- /**
4315
- * Create a hover gesture. hover() is different to .addEventListener("pointerenter")
4316
- * in that it has an easier syntax, filters out polyfilled touch events, interoperates
4317
- * with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
4318
- *
4319
- * @public
4320
- */
4321
- function hover(elementOrSelector, onHoverStart, options = {}) {
4322
- const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
4323
- const onPointerEnter = (enterEvent) => {
4324
- if (!isValidHover(enterEvent))
4325
- return;
4326
- const { target } = enterEvent;
4327
- const onHoverEnd = onHoverStart(target, enterEvent);
4328
- if (typeof onHoverEnd !== "function" || !target)
4329
- return;
4330
- const onPointerLeave = (leaveEvent) => {
4331
- if (!isValidHover(leaveEvent))
4332
- return;
4333
- onHoverEnd(leaveEvent);
4334
- target.removeEventListener("pointerleave", onPointerLeave);
4335
- };
4336
- target.addEventListener("pointerleave", onPointerLeave, eventOptions);
4227
+ function recordStats() {
4228
+ if (statsBuffer.value) {
4229
+ clearStatsBuffer();
4230
+ throw new Error("Stats are already being measured");
4231
+ }
4232
+ const newStatsBuffer = statsBuffer;
4233
+ newStatsBuffer.value = {
4234
+ frameloop: {
4235
+ setup: [],
4236
+ rate: [],
4237
+ read: [],
4238
+ resolveKeyframes: [],
4239
+ preUpdate: [],
4240
+ update: [],
4241
+ preRender: [],
4242
+ render: [],
4243
+ postRender: [],
4244
+ },
4245
+ animations: {
4246
+ mainThread: [],
4247
+ waapi: [],
4248
+ layout: [],
4249
+ },
4250
+ layoutProjection: {
4251
+ nodes: [],
4252
+ calculatedTargetDeltas: [],
4253
+ calculatedProjections: [],
4254
+ },
4337
4255
  };
4338
- elements.forEach((element) => {
4339
- element.addEventListener("pointerenter", onPointerEnter, eventOptions);
4340
- });
4341
- return cancel;
4256
+ newStatsBuffer.addProjectionMetrics = (metrics) => {
4257
+ const { layoutProjection } = newStatsBuffer.value;
4258
+ layoutProjection.nodes.push(metrics.nodes);
4259
+ layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
4260
+ layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
4261
+ };
4262
+ frame.postRender(record, true);
4263
+ return reportStats;
4342
4264
  }
4343
4265
 
4344
4266
  /**
4345
- * Recursively traverse up the tree to check whether the provided child node
4346
- * is the parent or a descendant of it.
4347
- *
4348
- * @param parent - Element to find
4349
- * @param child - Element to test against parent
4267
+ * Checks if an element is an SVG element in a way
4268
+ * that works across iframes
4350
4269
  */
4351
- const isNodeOrChild = (parent, child) => {
4352
- if (!child) {
4353
- return false;
4354
- }
4355
- else if (parent === child) {
4356
- return true;
4357
- }
4358
- else {
4359
- return isNodeOrChild(parent, child.parentElement);
4360
- }
4361
- };
4362
-
4363
- const isPrimaryPointer = (event) => {
4364
- if (event.pointerType === "mouse") {
4365
- return typeof event.button !== "number" || event.button <= 0;
4366
- }
4367
- else {
4368
- /**
4369
- * isPrimary is true for all mice buttons, whereas every touch point
4370
- * is regarded as its own input. So subsequent concurrent touch points
4371
- * will be false.
4372
- *
4373
- * Specifically match against false here as incomplete versions of
4374
- * PointerEvents in very old browser might have it set as undefined.
4375
- */
4376
- return event.isPrimary !== false;
4377
- }
4378
- };
4379
-
4380
- const focusableElements = new Set([
4381
- "BUTTON",
4382
- "INPUT",
4383
- "SELECT",
4384
- "TEXTAREA",
4385
- "A",
4386
- ]);
4387
- function isElementKeyboardAccessible(element) {
4388
- return (focusableElements.has(element.tagName) ||
4389
- element.tabIndex !== -1);
4270
+ function isSVGElement(element) {
4271
+ return isObject(element) && "ownerSVGElement" in element;
4390
4272
  }
4391
4273
 
4392
- const isPressing = new WeakSet();
4393
-
4394
4274
  /**
4395
- * Filter out events that are not "Enter" keys.
4275
+ * Checks if an element is specifically an SVGSVGElement (the root SVG element)
4276
+ * in a way that works across iframes
4396
4277
  */
4397
- function filterEvents(callback) {
4398
- return (event) => {
4399
- if (event.key !== "Enter")
4400
- return;
4401
- callback(event);
4402
- };
4278
+ function isSVGSVGElement(element) {
4279
+ return isSVGElement(element) && element.tagName === "svg";
4403
4280
  }
4404
- function firePointerEvent(target, type) {
4405
- target.dispatchEvent(new PointerEvent("pointer" + type, { isPrimary: true, bubbles: true }));
4281
+
4282
+ function transform(...args) {
4283
+ const useImmediate = !Array.isArray(args[0]);
4284
+ const argOffset = useImmediate ? 0 : -1;
4285
+ const inputValue = args[0 + argOffset];
4286
+ const inputRange = args[1 + argOffset];
4287
+ const outputRange = args[2 + argOffset];
4288
+ const options = args[3 + argOffset];
4289
+ const interpolator = interpolate(inputRange, outputRange, options);
4290
+ return useImmediate ? interpolator(inputValue) : interpolator;
4406
4291
  }
4407
- const enableKeyboardPress = (focusEvent, eventOptions) => {
4408
- const element = focusEvent.currentTarget;
4409
- if (!element)
4410
- return;
4411
- const handleKeydown = filterEvents(() => {
4412
- if (isPressing.has(element))
4413
- return;
4414
- firePointerEvent(element, "down");
4415
- const handleKeyup = filterEvents(() => {
4416
- firePointerEvent(element, "up");
4417
- });
4418
- const handleBlur = () => firePointerEvent(element, "cancel");
4419
- element.addEventListener("keyup", handleKeyup, eventOptions);
4420
- element.addEventListener("blur", handleBlur, eventOptions);
4292
+
4293
+ function subscribeValue(inputValues, outputValue, getLatest) {
4294
+ const update = () => outputValue.set(getLatest());
4295
+ const scheduleUpdate = () => frame.preRender(update, false, true);
4296
+ const subscriptions = inputValues.map((v) => v.on("change", scheduleUpdate));
4297
+ outputValue.on("destroy", () => {
4298
+ subscriptions.forEach((unsubscribe) => unsubscribe());
4299
+ cancelFrame(update);
4421
4300
  });
4422
- element.addEventListener("keydown", handleKeydown, eventOptions);
4301
+ }
4302
+
4303
+ /**
4304
+ * Create a `MotionValue` that transforms the output of other `MotionValue`s by
4305
+ * passing their latest values through a transform function.
4306
+ *
4307
+ * Whenever a `MotionValue` referred to in the provided function is updated,
4308
+ * it will be re-evaluated.
4309
+ *
4310
+ * ```jsx
4311
+ * const x = motionValue(0)
4312
+ * const y = transformValue(() => x.get() * 2) // double x
4313
+ * ```
4314
+ *
4315
+ * @param transformer - A transform function. This function must be pure with no side-effects or conditional statements.
4316
+ * @returns `MotionValue`
4317
+ *
4318
+ * @public
4319
+ */
4320
+ function transformValue(transform) {
4321
+ const collectedValues = [];
4423
4322
  /**
4424
- * Add an event listener that fires on blur to remove the keydown events.
4323
+ * Open session of collectMotionValues. Any MotionValue that calls get()
4324
+ * inside transform will be saved into this array.
4425
4325
  */
4426
- element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);
4427
- };
4326
+ collectMotionValues.current = collectedValues;
4327
+ const initialValue = transform();
4328
+ collectMotionValues.current = undefined;
4329
+ const value = motionValue(initialValue);
4330
+ subscribeValue(collectedValues, value, transform);
4331
+ return value;
4332
+ }
4428
4333
 
4429
4334
  /**
4430
- * Filter out events that are not primary pointer events, or are triggering
4431
- * while a Motion gesture is active.
4335
+ * Create a `MotionValue` that maps the output of another `MotionValue` by
4336
+ * mapping it from one range of values into another.
4337
+ *
4338
+ * @remarks
4339
+ *
4340
+ * Given an input range of `[-200, -100, 100, 200]` and an output range of
4341
+ * `[0, 1, 1, 0]`, the returned `MotionValue` will:
4342
+ *
4343
+ * - When provided a value between `-200` and `-100`, will return a value between `0` and `1`.
4344
+ * - When provided a value between `-100` and `100`, will return `1`.
4345
+ * - When provided a value between `100` and `200`, will return a value between `1` and `0`
4346
+ *
4347
+ * The input range must be a linear series of numbers. The output range
4348
+ * can be any value type supported by Motion: numbers, colors, shadows, etc.
4349
+ *
4350
+ * Every value in the output range must be of the same type and in the same format.
4351
+ *
4352
+ * ```jsx
4353
+ * const x = motionValue(0)
4354
+ * const xRange = [-200, -100, 100, 200]
4355
+ * const opacityRange = [0, 1, 1, 0]
4356
+ * const opacity = mapValue(x, xRange, opacityRange)
4357
+ * ```
4358
+ *
4359
+ * @param inputValue - `MotionValue`
4360
+ * @param inputRange - A linear series of numbers (either all increasing or decreasing)
4361
+ * @param outputRange - A series of numbers, colors or strings. Must be the same length as `inputRange`.
4362
+ * @param options -
4363
+ *
4364
+ * - clamp: boolean. Clamp values to within the given range. Defaults to `true`
4365
+ * - ease: EasingFunction[]. Easing functions to use on the interpolations between each value in the input and output ranges. If provided as an array, the array must be one item shorter than the input and output ranges, as the easings apply to the transition between each.
4366
+ *
4367
+ * @returns `MotionValue`
4368
+ *
4369
+ * @public
4432
4370
  */
4433
- function isValidPressEvent(event) {
4434
- return isPrimaryPointer(event) && !isDragActive();
4371
+ function mapValue(inputValue, inputRange, outputRange, options) {
4372
+ const map = transform(inputRange, outputRange, options);
4373
+ return transformValue(() => map(inputValue.get()));
4435
4374
  }
4375
+
4376
+ const isMotionValue = (value) => Boolean(value && value.getVelocity);
4377
+
4436
4378
  /**
4437
- * Create a press gesture.
4438
- *
4439
- * Press is different to `"pointerdown"`, `"pointerup"` in that it
4440
- * automatically filters out secondary pointer events like right
4441
- * click and multitouch.
4379
+ * Create a `MotionValue` that animates to its latest value using a spring.
4380
+ * Can either be a value or track another `MotionValue`.
4442
4381
  *
4443
- * It also adds accessibility support for keyboards, where
4444
- * an element with a press gesture will receive focus and
4445
- * trigger on Enter `"keydown"` and `"keyup"` events.
4382
+ * ```jsx
4383
+ * const x = motionValue(0)
4384
+ * const y = transformValue(() => x.get() * 2) // double x
4385
+ * ```
4446
4386
  *
4447
- * This is different to a browser's `"click"` event, which does
4448
- * respond to keyboards but only for the `"click"` itself, rather
4449
- * than the press start and end/cancel. The element also needs
4450
- * to be focusable for this to work, whereas a press gesture will
4451
- * make an element focusable by default.
4387
+ * @param transformer - A transform function. This function must be pure with no side-effects or conditional statements.
4388
+ * @returns `MotionValue`
4452
4389
  *
4453
4390
  * @public
4454
4391
  */
4455
- function press(targetOrSelector, onPressStart, options = {}) {
4456
- const [targets, eventOptions, cancelEvents] = setupGesture(targetOrSelector, options);
4457
- const startPress = (startEvent) => {
4458
- const target = startEvent.currentTarget;
4459
- if (!isValidPressEvent(startEvent))
4460
- return;
4461
- isPressing.add(target);
4462
- const onPressEnd = onPressStart(target, startEvent);
4463
- const onPointerEnd = (endEvent, success) => {
4464
- window.removeEventListener("pointerup", onPointerUp);
4465
- window.removeEventListener("pointercancel", onPointerCancel);
4466
- if (isPressing.has(target)) {
4467
- isPressing.delete(target);
4468
- }
4469
- if (!isValidPressEvent(endEvent)) {
4470
- return;
4471
- }
4472
- if (typeof onPressEnd === "function") {
4473
- onPressEnd(endEvent, { success });
4474
- }
4475
- };
4476
- const onPointerUp = (upEvent) => {
4477
- onPointerEnd(upEvent, target === window ||
4478
- target === document ||
4479
- options.useGlobalTarget ||
4480
- isNodeOrChild(target, upEvent.target));
4481
- };
4482
- const onPointerCancel = (cancelEvent) => {
4483
- onPointerEnd(cancelEvent, false);
4484
- };
4485
- window.addEventListener("pointerup", onPointerUp, eventOptions);
4486
- window.addEventListener("pointercancel", onPointerCancel, eventOptions);
4487
- };
4488
- targets.forEach((target) => {
4489
- const pointerDownTarget = options.useGlobalTarget ? window : target;
4490
- pointerDownTarget.addEventListener("pointerdown", startPress, eventOptions);
4491
- if (target instanceof HTMLElement) {
4492
- target.addEventListener("focus", (event) => enableKeyboardPress(event, eventOptions));
4493
- if (!isElementKeyboardAccessible(target) &&
4494
- !target.hasAttribute("tabindex")) {
4495
- target.tabIndex = 0;
4496
- }
4392
+ function springValue(source, options) {
4393
+ const initialValue = isMotionValue(source) ? source.get() : source;
4394
+ const value = motionValue(initialValue);
4395
+ attachSpring(value, source, options);
4396
+ return value;
4397
+ }
4398
+ function attachSpring(value, source, options) {
4399
+ const initialValue = value.get();
4400
+ let activeAnimation = null;
4401
+ let latestValue = initialValue;
4402
+ let latestSetter;
4403
+ const unit = typeof initialValue === "string"
4404
+ ? initialValue.replace(/[\d.-]/g, "")
4405
+ : undefined;
4406
+ const stopAnimation = () => {
4407
+ if (activeAnimation) {
4408
+ activeAnimation.stop();
4409
+ activeAnimation = null;
4497
4410
  }
4498
- });
4499
- return cancelEvents;
4411
+ };
4412
+ const startAnimation = () => {
4413
+ stopAnimation();
4414
+ activeAnimation = new JSAnimation({
4415
+ keyframes: [asNumber$1(value.get()), asNumber$1(latestValue)],
4416
+ velocity: value.getVelocity(),
4417
+ type: "spring",
4418
+ restDelta: 0.001,
4419
+ restSpeed: 0.01,
4420
+ ...options,
4421
+ onUpdate: latestSetter,
4422
+ });
4423
+ };
4424
+ value.attach((v, set) => {
4425
+ latestValue = v;
4426
+ latestSetter = (latest) => set(parseValue(latest, unit));
4427
+ frame.postRender(startAnimation);
4428
+ return value.get();
4429
+ }, stopAnimation);
4430
+ let unsubscribe = undefined;
4431
+ if (isMotionValue(source)) {
4432
+ unsubscribe = source.on("change", (v) => value.set(parseValue(v, unit)));
4433
+ value.on("destroy", unsubscribe);
4434
+ }
4435
+ return unsubscribe;
4436
+ }
4437
+ function parseValue(v, unit) {
4438
+ return unit ? v + unit : v;
4439
+ }
4440
+ function asNumber$1(v) {
4441
+ return typeof v === "number" ? v : parseFloat(v);
4500
4442
  }
4501
4443
 
4502
- function getComputedStyle$2(element, name) {
4503
- const computedStyle = window.getComputedStyle(element);
4504
- return isCSSVar(name)
4505
- ? computedStyle.getPropertyValue(name)
4506
- : computedStyle[name];
4444
+ /**
4445
+ * A list of all ValueTypes
4446
+ */
4447
+ const valueTypes = [...dimensionValueTypes, color, complex];
4448
+ /**
4449
+ * Tests a value against the list of ValueTypes
4450
+ */
4451
+ const findValueType = (v) => valueTypes.find(testValueType(v));
4452
+
4453
+ function chooseLayerType(valueName) {
4454
+ if (valueName === "layout")
4455
+ return "group";
4456
+ if (valueName === "enter" || valueName === "new")
4457
+ return "new";
4458
+ if (valueName === "exit" || valueName === "old")
4459
+ return "old";
4460
+ return "group";
4507
4461
  }
4508
4462
 
4509
- function observeTimeline(update, timeline) {
4510
- let prevProgress;
4511
- const onFrame = () => {
4512
- const { currentTime } = timeline;
4513
- const percentage = currentTime === null ? 0 : currentTime.value;
4514
- const progress = percentage / 100;
4515
- if (prevProgress !== progress) {
4516
- update(progress);
4463
+ let pendingRules = {};
4464
+ let style = null;
4465
+ const css = {
4466
+ set: (selector, values) => {
4467
+ pendingRules[selector] = values;
4468
+ },
4469
+ commit: () => {
4470
+ if (!style) {
4471
+ style = document.createElement("style");
4472
+ style.id = "motion-view";
4517
4473
  }
4518
- prevProgress = progress;
4519
- };
4520
- frame.preUpdate(onFrame, true);
4521
- return () => cancelFrame(onFrame);
4522
- }
4474
+ let cssText = "";
4475
+ for (const selector in pendingRules) {
4476
+ const rule = pendingRules[selector];
4477
+ cssText += `${selector} {\n`;
4478
+ for (const [property, value] of Object.entries(rule)) {
4479
+ cssText += ` ${property}: ${value};\n`;
4480
+ }
4481
+ cssText += "}\n";
4482
+ }
4483
+ style.textContent = cssText;
4484
+ document.head.appendChild(style);
4485
+ pendingRules = {};
4486
+ },
4487
+ remove: () => {
4488
+ if (style && style.parentElement) {
4489
+ style.parentElement.removeChild(style);
4490
+ }
4491
+ },
4492
+ };
4523
4493
 
4524
- function record() {
4525
- const { value } = statsBuffer;
4526
- if (value === null) {
4527
- cancelFrame(record);
4528
- return;
4529
- }
4530
- value.frameloop.rate.push(frameData.delta);
4531
- value.animations.mainThread.push(activeAnimations.mainThread);
4532
- value.animations.waapi.push(activeAnimations.waapi);
4533
- value.animations.layout.push(activeAnimations.layout);
4494
+ function getLayerName(pseudoElement) {
4495
+ const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
4496
+ if (!match)
4497
+ return null;
4498
+ return { layer: match[2], type: match[1] };
4534
4499
  }
4535
- function mean(values) {
4536
- return values.reduce((acc, value) => acc + value, 0) / values.length;
4500
+
4501
+ function filterViewAnimations(animation) {
4502
+ const { effect } = animation;
4503
+ if (!effect)
4504
+ return false;
4505
+ return (effect.target === document.documentElement &&
4506
+ effect.pseudoElement?.startsWith("::view-transition"));
4537
4507
  }
4538
- function summarise(values, calcAverage = mean) {
4539
- if (values.length === 0) {
4540
- return {
4541
- min: 0,
4542
- max: 0,
4543
- avg: 0,
4544
- };
4545
- }
4546
- return {
4547
- min: Math.min(...values),
4548
- max: Math.max(...values),
4549
- avg: calcAverage(values),
4550
- };
4508
+ function getViewAnimations() {
4509
+ return document.getAnimations().filter(filterViewAnimations);
4551
4510
  }
4552
- const msToFps = (ms) => Math.round(1000 / ms);
4553
- function clearStatsBuffer() {
4554
- statsBuffer.value = null;
4555
- statsBuffer.addProjectionMetrics = null;
4511
+
4512
+ function hasTarget(target, targets) {
4513
+ return targets.has(target) && Object.keys(targets.get(target)).length > 0;
4556
4514
  }
4557
- function reportStats() {
4558
- const { value } = statsBuffer;
4559
- if (!value) {
4560
- throw new Error("Stats are not being measured");
4515
+
4516
+ const definitionNames = ["layout", "enter", "exit", "new", "old"];
4517
+ function startViewAnimation(builder) {
4518
+ const { update, targets, options: defaultOptions } = builder;
4519
+ if (!document.startViewTransition) {
4520
+ return new Promise(async (resolve) => {
4521
+ await update();
4522
+ resolve(new GroupAnimation([]));
4523
+ });
4561
4524
  }
4562
- clearStatsBuffer();
4563
- cancelFrame(record);
4564
- const summary = {
4565
- frameloop: {
4566
- setup: summarise(value.frameloop.setup),
4567
- rate: summarise(value.frameloop.rate),
4568
- read: summarise(value.frameloop.read),
4569
- resolveKeyframes: summarise(value.frameloop.resolveKeyframes),
4570
- preUpdate: summarise(value.frameloop.preUpdate),
4571
- update: summarise(value.frameloop.update),
4572
- preRender: summarise(value.frameloop.preRender),
4573
- render: summarise(value.frameloop.render),
4574
- postRender: summarise(value.frameloop.postRender),
4575
- },
4576
- animations: {
4577
- mainThread: summarise(value.animations.mainThread),
4578
- waapi: summarise(value.animations.waapi),
4579
- layout: summarise(value.animations.layout),
4580
- },
4581
- layoutProjection: {
4582
- nodes: summarise(value.layoutProjection.nodes),
4583
- calculatedTargetDeltas: summarise(value.layoutProjection.calculatedTargetDeltas),
4584
- calculatedProjections: summarise(value.layoutProjection.calculatedProjections),
4585
- },
4586
- };
4525
+ // TODO: Go over existing targets and ensure they all have ids
4587
4526
  /**
4588
- * Convert the rate to FPS
4527
+ * If we don't have any animations defined for the root target,
4528
+ * remove it from being captured.
4589
4529
  */
4590
- const { rate } = summary.frameloop;
4591
- rate.min = msToFps(rate.min);
4592
- rate.max = msToFps(rate.max);
4593
- rate.avg = msToFps(rate.avg);
4594
- [rate.min, rate.max] = [rate.max, rate.min];
4595
- return summary;
4530
+ if (!hasTarget("root", targets)) {
4531
+ css.set(":root", {
4532
+ "view-transition-name": "none",
4533
+ });
4534
+ }
4535
+ /**
4536
+ * Set the timing curve to linear for all view transition layers.
4537
+ * This gets baked into the keyframes, which can't be changed
4538
+ * without breaking the generated animation.
4539
+ *
4540
+ * This allows us to set easing via updateTiming - which can be changed.
4541
+ */
4542
+ css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
4543
+ css.commit(); // Write
4544
+ const transition = document.startViewTransition(async () => {
4545
+ await update();
4546
+ // TODO: Go over new targets and ensure they all have ids
4547
+ });
4548
+ transition.finished.finally(() => {
4549
+ css.remove(); // Write
4550
+ });
4551
+ return new Promise((resolve) => {
4552
+ transition.ready.then(() => {
4553
+ const generatedViewAnimations = getViewAnimations();
4554
+ const animations = [];
4555
+ /**
4556
+ * Create animations for each of our explicitly-defined subjects.
4557
+ */
4558
+ targets.forEach((definition, target) => {
4559
+ // TODO: If target is not "root", resolve elements
4560
+ // and iterate over each
4561
+ for (const key of definitionNames) {
4562
+ if (!definition[key])
4563
+ continue;
4564
+ const { keyframes, options } = definition[key];
4565
+ for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
4566
+ if (!valueKeyframes)
4567
+ continue;
4568
+ const valueOptions = {
4569
+ ...getValueTransition$1(defaultOptions, valueName),
4570
+ ...getValueTransition$1(options, valueName),
4571
+ };
4572
+ const type = chooseLayerType(key);
4573
+ /**
4574
+ * If this is an opacity animation, and keyframes are not an array,
4575
+ * we need to convert them into an array and set an initial value.
4576
+ */
4577
+ if (valueName === "opacity" &&
4578
+ !Array.isArray(valueKeyframes)) {
4579
+ const initialValue = type === "new" ? 0 : 1;
4580
+ valueKeyframes = [initialValue, valueKeyframes];
4581
+ }
4582
+ /**
4583
+ * Resolve stagger function if provided.
4584
+ */
4585
+ if (typeof valueOptions.delay === "function") {
4586
+ valueOptions.delay = valueOptions.delay(0, 1);
4587
+ }
4588
+ valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
4589
+ valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
4590
+ const animation = new NativeAnimation({
4591
+ ...valueOptions,
4592
+ element: document.documentElement,
4593
+ name: valueName,
4594
+ pseudoElement: `::view-transition-${type}(${target})`,
4595
+ keyframes: valueKeyframes,
4596
+ });
4597
+ animations.push(animation);
4598
+ }
4599
+ }
4600
+ });
4601
+ /**
4602
+ * Handle browser generated animations
4603
+ */
4604
+ for (const animation of generatedViewAnimations) {
4605
+ if (animation.playState === "finished")
4606
+ continue;
4607
+ const { effect } = animation;
4608
+ if (!effect || !(effect instanceof KeyframeEffect))
4609
+ continue;
4610
+ const { pseudoElement } = effect;
4611
+ if (!pseudoElement)
4612
+ continue;
4613
+ const name = getLayerName(pseudoElement);
4614
+ if (!name)
4615
+ continue;
4616
+ const targetDefinition = targets.get(name.layer);
4617
+ if (!targetDefinition) {
4618
+ /**
4619
+ * If transition name is group then update the timing of the animation
4620
+ * whereas if it's old or new then we could possibly replace it using
4621
+ * the above method.
4622
+ */
4623
+ const transitionName = name.type === "group" ? "layout" : "";
4624
+ let animationTransition = {
4625
+ ...getValueTransition$1(defaultOptions, transitionName),
4626
+ };
4627
+ animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
4628
+ animationTransition =
4629
+ applyGeneratorOptions(animationTransition);
4630
+ const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
4631
+ effect.updateTiming({
4632
+ delay: secondsToMilliseconds(animationTransition.delay ?? 0),
4633
+ duration: animationTransition.duration,
4634
+ easing,
4635
+ });
4636
+ animations.push(new NativeAnimationWrapper(animation));
4637
+ }
4638
+ else if (hasOpacity(targetDefinition, "enter") &&
4639
+ hasOpacity(targetDefinition, "exit") &&
4640
+ effect
4641
+ .getKeyframes()
4642
+ .some((keyframe) => keyframe.mixBlendMode)) {
4643
+ animations.push(new NativeAnimationWrapper(animation));
4644
+ }
4645
+ else {
4646
+ animation.cancel();
4647
+ }
4648
+ }
4649
+ resolve(new GroupAnimation(animations));
4650
+ });
4651
+ });
4596
4652
  }
4597
- function recordStats() {
4598
- if (statsBuffer.value) {
4599
- clearStatsBuffer();
4600
- throw new Error("Stats are already being measured");
4601
- }
4602
- const newStatsBuffer = statsBuffer;
4603
- newStatsBuffer.value = {
4604
- frameloop: {
4605
- setup: [],
4606
- rate: [],
4607
- read: [],
4608
- resolveKeyframes: [],
4609
- preUpdate: [],
4610
- update: [],
4611
- preRender: [],
4612
- render: [],
4613
- postRender: [],
4614
- },
4615
- animations: {
4616
- mainThread: [],
4617
- waapi: [],
4618
- layout: [],
4619
- },
4620
- layoutProjection: {
4621
- nodes: [],
4622
- calculatedTargetDeltas: [],
4623
- calculatedProjections: [],
4624
- },
4625
- };
4626
- newStatsBuffer.addProjectionMetrics = (metrics) => {
4627
- const { layoutProjection } = newStatsBuffer.value;
4628
- layoutProjection.nodes.push(metrics.nodes);
4629
- layoutProjection.calculatedTargetDeltas.push(metrics.calculatedTargetDeltas);
4630
- layoutProjection.calculatedProjections.push(metrics.calculatedProjections);
4631
- };
4632
- frame.postRender(record, true);
4633
- return reportStats;
4653
+ function hasOpacity(target, key) {
4654
+ return target?.[key]?.keyframes.opacity;
4634
4655
  }
4635
4656
 
4636
- function transform(...args) {
4637
- const useImmediate = !Array.isArray(args[0]);
4638
- const argOffset = useImmediate ? 0 : -1;
4639
- const inputValue = args[0 + argOffset];
4640
- const inputRange = args[1 + argOffset];
4641
- const outputRange = args[2 + argOffset];
4642
- const options = args[3 + argOffset];
4643
- const interpolator = interpolate(inputRange, outputRange, options);
4644
- return useImmediate ? interpolator(inputValue) : interpolator;
4657
+ let builders = [];
4658
+ let current = null;
4659
+ function next() {
4660
+ current = null;
4661
+ const [nextBuilder] = builders;
4662
+ if (nextBuilder)
4663
+ start(nextBuilder);
4645
4664
  }
4646
-
4647
- function subscribeValue(inputValues, outputValue, getLatest) {
4648
- const update = () => outputValue.set(getLatest());
4649
- const scheduleUpdate = () => frame.preRender(update, false, true);
4650
- const subscriptions = inputValues.map((v) => v.on("change", scheduleUpdate));
4651
- outputValue.on("destroy", () => {
4652
- subscriptions.forEach((unsubscribe) => unsubscribe());
4653
- cancelFrame(update);
4665
+ function start(builder) {
4666
+ removeItem(builders, builder);
4667
+ current = builder;
4668
+ startViewAnimation(builder).then((animation) => {
4669
+ builder.notifyReady(animation);
4670
+ animation.finished.finally(next);
4654
4671
  });
4655
4672
  }
4656
-
4657
- /**
4658
- * Create a `MotionValue` that transforms the output of other `MotionValue`s by
4659
- * passing their latest values through a transform function.
4660
- *
4661
- * Whenever a `MotionValue` referred to in the provided function is updated,
4662
- * it will be re-evaluated.
4663
- *
4664
- * ```jsx
4665
- * const x = motionValue(0)
4666
- * const y = transformValue(() => x.get() * 2) // double x
4667
- * ```
4668
- *
4669
- * @param transformer - A transform function. This function must be pure with no side-effects or conditional statements.
4670
- * @returns `MotionValue`
4671
- *
4672
- * @public
4673
- */
4674
- function transformValue(transform) {
4675
- const collectedValues = [];
4673
+ function processQueue() {
4676
4674
  /**
4677
- * Open session of collectMotionValues. Any MotionValue that calls get()
4678
- * inside transform will be saved into this array.
4675
+ * Iterate backwards over the builders array. We can ignore the
4676
+ * "wait" animations. If we have an interrupting animation in the
4677
+ * queue then we need to batch all preceeding animations into it.
4678
+ * Currently this only batches the update functions but will also
4679
+ * need to batch the targets.
4679
4680
  */
4680
- collectMotionValues.current = collectedValues;
4681
- const initialValue = transform();
4682
- collectMotionValues.current = undefined;
4683
- const value = motionValue(initialValue);
4684
- subscribeValue(collectedValues, value, transform);
4685
- return value;
4686
- }
4687
-
4688
- /**
4689
- * Create a `MotionValue` that maps the output of another `MotionValue` by
4690
- * mapping it from one range of values into another.
4691
- *
4692
- * @remarks
4693
- *
4694
- * Given an input range of `[-200, -100, 100, 200]` and an output range of
4695
- * `[0, 1, 1, 0]`, the returned `MotionValue` will:
4696
- *
4697
- * - When provided a value between `-200` and `-100`, will return a value between `0` and `1`.
4698
- * - When provided a value between `-100` and `100`, will return `1`.
4699
- * - When provided a value between `100` and `200`, will return a value between `1` and `0`
4700
- *
4701
- * The input range must be a linear series of numbers. The output range
4702
- * can be any value type supported by Motion: numbers, colors, shadows, etc.
4703
- *
4704
- * Every value in the output range must be of the same type and in the same format.
4705
- *
4706
- * ```jsx
4707
- * const x = motionValue(0)
4708
- * const xRange = [-200, -100, 100, 200]
4709
- * const opacityRange = [0, 1, 1, 0]
4710
- * const opacity = mapValue(x, xRange, opacityRange)
4711
- * ```
4712
- *
4713
- * @param inputValue - `MotionValue`
4714
- * @param inputRange - A linear series of numbers (either all increasing or decreasing)
4715
- * @param outputRange - A series of numbers, colors or strings. Must be the same length as `inputRange`.
4716
- * @param options -
4717
- *
4718
- * - clamp: boolean. Clamp values to within the given range. Defaults to `true`
4719
- * - ease: EasingFunction[]. Easing functions to use on the interpolations between each value in the input and output ranges. If provided as an array, the array must be one item shorter than the input and output ranges, as the easings apply to the transition between each.
4720
- *
4721
- * @returns `MotionValue`
4722
- *
4723
- * @public
4724
- */
4725
- function mapValue(inputValue, inputRange, outputRange, options) {
4726
- const map = transform(inputRange, outputRange, options);
4727
- return transformValue(() => map(inputValue.get()));
4728
- }
4729
-
4730
- const isMotionValue = (value) => Boolean(value && value.getVelocity);
4731
-
4732
- /**
4733
- * Create a `MotionValue` that animates to its latest value using a spring.
4734
- * Can either be a value or track another `MotionValue`.
4735
- *
4736
- * ```jsx
4737
- * const x = motionValue(0)
4738
- * const y = transformValue(() => x.get() * 2) // double x
4739
- * ```
4740
- *
4741
- * @param transformer - A transform function. This function must be pure with no side-effects or conditional statements.
4742
- * @returns `MotionValue`
4743
- *
4744
- * @public
4745
- */
4746
- function springValue(source, options) {
4747
- const initialValue = isMotionValue(source) ? source.get() : source;
4748
- const value = motionValue(initialValue);
4749
- attachSpring(value, source, options);
4750
- return value;
4751
- }
4752
- function attachSpring(value, source, options) {
4753
- const initialValue = value.get();
4754
- let activeAnimation = null;
4755
- let latestValue = initialValue;
4756
- let latestSetter;
4757
- const unit = typeof initialValue === "string"
4758
- ? initialValue.replace(/[\d.-]/g, "")
4759
- : undefined;
4760
- const stopAnimation = () => {
4761
- if (activeAnimation) {
4762
- activeAnimation.stop();
4763
- activeAnimation = null;
4681
+ for (let i = builders.length - 1; i >= 0; i--) {
4682
+ const builder = builders[i];
4683
+ const { interrupt } = builder.options;
4684
+ if (interrupt === "immediate") {
4685
+ const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update);
4686
+ const remaining = builders.slice(i + 1);
4687
+ builder.update = () => {
4688
+ batchedUpdates.forEach((update) => update());
4689
+ };
4690
+ // Put the current builder at the front, followed by any "wait" builders
4691
+ builders = [builder, ...remaining];
4692
+ break;
4764
4693
  }
4765
- };
4766
- const startAnimation = () => {
4767
- stopAnimation();
4768
- activeAnimation = new JSAnimation({
4769
- keyframes: [asNumber$1(value.get()), asNumber$1(latestValue)],
4770
- velocity: value.getVelocity(),
4771
- type: "spring",
4772
- restDelta: 0.001,
4773
- restSpeed: 0.01,
4774
- ...options,
4775
- onUpdate: latestSetter,
4776
- });
4777
- };
4778
- value.attach((v, set) => {
4779
- latestValue = v;
4780
- latestSetter = (latest) => set(parseValue(latest, unit));
4781
- frame.postRender(startAnimation);
4782
- return value.get();
4783
- }, stopAnimation);
4784
- let unsubscribe = undefined;
4785
- if (isMotionValue(source)) {
4786
- unsubscribe = source.on("change", (v) => value.set(parseValue(v, unit)));
4787
- value.on("destroy", unsubscribe);
4788
4694
  }
4789
- return unsubscribe;
4695
+ if (!current || builders[0]?.options.interrupt === "immediate") {
4696
+ next();
4697
+ }
4790
4698
  }
4791
- function parseValue(v, unit) {
4792
- return unit ? v + unit : v;
4699
+ function addToQueue(builder) {
4700
+ builders.push(builder);
4701
+ microtask.render(processQueue);
4793
4702
  }
4794
- function asNumber$1(v) {
4795
- return typeof v === "number" ? v : parseFloat(v);
4703
+
4704
+ class ViewTransitionBuilder {
4705
+ constructor(update, options = {}) {
4706
+ this.currentTarget = "root";
4707
+ this.targets = new Map();
4708
+ this.notifyReady = noop;
4709
+ this.readyPromise = new Promise((resolve) => {
4710
+ this.notifyReady = resolve;
4711
+ });
4712
+ this.update = update;
4713
+ this.options = {
4714
+ interrupt: "wait",
4715
+ ...options,
4716
+ };
4717
+ addToQueue(this);
4718
+ }
4719
+ get(selector) {
4720
+ this.currentTarget = selector;
4721
+ return this;
4722
+ }
4723
+ layout(keyframes, options) {
4724
+ this.updateTarget("layout", keyframes, options);
4725
+ return this;
4726
+ }
4727
+ new(keyframes, options) {
4728
+ this.updateTarget("new", keyframes, options);
4729
+ return this;
4730
+ }
4731
+ old(keyframes, options) {
4732
+ this.updateTarget("old", keyframes, options);
4733
+ return this;
4734
+ }
4735
+ enter(keyframes, options) {
4736
+ this.updateTarget("enter", keyframes, options);
4737
+ return this;
4738
+ }
4739
+ exit(keyframes, options) {
4740
+ this.updateTarget("exit", keyframes, options);
4741
+ return this;
4742
+ }
4743
+ crossfade(options) {
4744
+ this.updateTarget("enter", { opacity: 1 }, options);
4745
+ this.updateTarget("exit", { opacity: 0 }, options);
4746
+ return this;
4747
+ }
4748
+ updateTarget(target, keyframes, options = {}) {
4749
+ const { currentTarget, targets } = this;
4750
+ if (!targets.has(currentTarget)) {
4751
+ targets.set(currentTarget, {});
4752
+ }
4753
+ const targetData = targets.get(currentTarget);
4754
+ targetData[target] = { keyframes, options };
4755
+ }
4756
+ then(resolve, reject) {
4757
+ return this.readyPromise.then(resolve, reject);
4758
+ }
4759
+ }
4760
+ function animateView(update, defaultOptions = {}) {
4761
+ return new ViewTransitionBuilder(update, defaultOptions);
4796
4762
  }
4797
4763
 
4798
4764
  /**
4799
- * A list of all ValueTypes
4765
+ * @deprecated
4766
+ *
4767
+ * Import as `frame` instead.
4800
4768
  */
4801
- const valueTypes = [...dimensionValueTypes, color, complex];
4769
+ const sync = frame;
4802
4770
  /**
4803
- * Tests a value against the list of ValueTypes
4771
+ * @deprecated
4772
+ *
4773
+ * Use cancelFrame(callback) instead.
4804
4774
  */
4805
- const findValueType = (v) => valueTypes.find(testValueType(v));
4775
+ const cancelSync = stepsOrder.reduce((acc, key) => {
4776
+ acc[key] = (process) => cancelFrame(process);
4777
+ return acc;
4778
+ }, {});
4806
4779
 
4807
- function chooseLayerType(valueName) {
4808
- if (valueName === "layout")
4809
- return "group";
4810
- if (valueName === "enter" || valueName === "new")
4811
- return "new";
4812
- if (valueName === "exit" || valueName === "old")
4813
- return "old";
4814
- return "group";
4815
- }
4780
+ /**
4781
+ * @public
4782
+ */
4783
+ const MotionConfigContext = React$1.createContext({
4784
+ transformPagePoint: (p) => p,
4785
+ isStatic: false,
4786
+ reducedMotion: "never",
4787
+ });
4816
4788
 
4817
- let pendingRules = {};
4818
- let style = null;
4819
- const css = {
4820
- set: (selector, values) => {
4821
- pendingRules[selector] = values;
4822
- },
4823
- commit: () => {
4824
- if (!style) {
4825
- style = document.createElement("style");
4826
- style.id = "motion-view";
4827
- }
4828
- let cssText = "";
4829
- for (const selector in pendingRules) {
4830
- const rule = pendingRules[selector];
4831
- cssText += `${selector} {\n`;
4832
- for (const [property, value] of Object.entries(rule)) {
4833
- cssText += ` ${property}: ${value};\n`;
4834
- }
4835
- cssText += "}\n";
4836
- }
4837
- style.textContent = cssText;
4838
- document.head.appendChild(style);
4839
- pendingRules = {};
4840
- },
4841
- remove: () => {
4842
- if (style && style.parentElement) {
4843
- style.parentElement.removeChild(style);
4789
+ /**
4790
+ * Measurement functionality has to be within a separate component
4791
+ * to leverage snapshot lifecycle.
4792
+ */
4793
+ class PopChildMeasure extends React__namespace.Component {
4794
+ getSnapshotBeforeUpdate(prevProps) {
4795
+ const element = this.props.childRef.current;
4796
+ if (element && prevProps.isPresent && !this.props.isPresent) {
4797
+ const parent = element.offsetParent;
4798
+ const parentWidth = isHTMLElement(parent)
4799
+ ? parent.offsetWidth || 0
4800
+ : 0;
4801
+ const size = this.props.sizeRef.current;
4802
+ size.height = element.offsetHeight || 0;
4803
+ size.width = element.offsetWidth || 0;
4804
+ size.top = element.offsetTop;
4805
+ size.left = element.offsetLeft;
4806
+ size.right = parentWidth - size.width - size.left;
4844
4807
  }
4845
- },
4846
- };
4847
-
4848
- function getLayerName(pseudoElement) {
4849
- const match = pseudoElement.match(/::view-transition-(old|new|group|image-pair)\((.*?)\)/);
4850
- if (!match)
4851
4808
  return null;
4852
- return { layer: match[2], type: match[1] };
4853
- }
4854
-
4855
- function filterViewAnimations(animation) {
4856
- const { effect } = animation;
4857
- if (!effect)
4858
- return false;
4859
- return (effect.target === document.documentElement &&
4860
- effect.pseudoElement?.startsWith("::view-transition"));
4861
- }
4862
- function getViewAnimations() {
4863
- return document.getAnimations().filter(filterViewAnimations);
4864
- }
4865
-
4866
- function hasTarget(target, targets) {
4867
- return targets.has(target) && Object.keys(targets.get(target)).length > 0;
4868
- }
4869
-
4870
- const definitionNames = ["layout", "enter", "exit", "new", "old"];
4871
- function startViewAnimation(builder) {
4872
- const { update, targets, options: defaultOptions } = builder;
4873
- if (!document.startViewTransition) {
4874
- return new Promise(async (resolve) => {
4875
- await update();
4876
- resolve(new GroupAnimation([]));
4877
- });
4878
4809
  }
4879
- // TODO: Go over existing targets and ensure they all have ids
4880
4810
  /**
4881
- * If we don't have any animations defined for the root target,
4882
- * remove it from being captured.
4811
+ * Required with getSnapshotBeforeUpdate to stop React complaining.
4812
+ */
4813
+ componentDidUpdate() { }
4814
+ render() {
4815
+ return this.props.children;
4816
+ }
4817
+ }
4818
+ function PopChild({ children, isPresent, anchorX }) {
4819
+ const id = React$1.useId();
4820
+ const ref = React$1.useRef(null);
4821
+ const size = React$1.useRef({
4822
+ width: 0,
4823
+ height: 0,
4824
+ top: 0,
4825
+ left: 0,
4826
+ right: 0,
4827
+ });
4828
+ const { nonce } = React$1.useContext(MotionConfigContext);
4829
+ /**
4830
+ * We create and inject a style block so we can apply this explicit
4831
+ * sizing in a non-destructive manner by just deleting the style block.
4832
+ *
4833
+ * We can't apply size via render as the measurement happens
4834
+ * in getSnapshotBeforeUpdate (post-render), likewise if we apply the
4835
+ * styles directly on the DOM node, we might be overwriting
4836
+ * styles set via the style prop.
4837
+ */
4838
+ React$1.useInsertionEffect(() => {
4839
+ const { width, height, top, left, right } = size.current;
4840
+ if (isPresent || !ref.current || !width || !height)
4841
+ return;
4842
+ const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`;
4843
+ ref.current.dataset.motionPopId = id;
4844
+ const style = document.createElement("style");
4845
+ if (nonce)
4846
+ style.nonce = nonce;
4847
+ document.head.appendChild(style);
4848
+ if (style.sheet) {
4849
+ style.sheet.insertRule(`
4850
+ [data-motion-pop-id="${id}"] {
4851
+ position: absolute !important;
4852
+ width: ${width}px !important;
4853
+ height: ${height}px !important;
4854
+ ${x}px !important;
4855
+ top: ${top}px !important;
4856
+ }
4857
+ `);
4858
+ }
4859
+ return () => {
4860
+ if (document.head.contains(style)) {
4861
+ document.head.removeChild(style);
4862
+ }
4863
+ };
4864
+ }, [isPresent]);
4865
+ return (jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React__namespace.cloneElement(children, { ref }) }));
4866
+ }
4867
+
4868
+ const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, }) => {
4869
+ const presenceChildren = useConstant(newChildrenMap);
4870
+ const id = React$1.useId();
4871
+ let isReusedContext = true;
4872
+ let context = React$1.useMemo(() => {
4873
+ isReusedContext = false;
4874
+ return {
4875
+ id,
4876
+ initial,
4877
+ isPresent,
4878
+ custom,
4879
+ onExitComplete: (childId) => {
4880
+ presenceChildren.set(childId, true);
4881
+ for (const isComplete of presenceChildren.values()) {
4882
+ if (!isComplete)
4883
+ return; // can stop searching when any is incomplete
4884
+ }
4885
+ onExitComplete && onExitComplete();
4886
+ },
4887
+ register: (childId) => {
4888
+ presenceChildren.set(childId, false);
4889
+ return () => presenceChildren.delete(childId);
4890
+ },
4891
+ };
4892
+ }, [isPresent, presenceChildren, onExitComplete]);
4893
+ /**
4894
+ * If the presence of a child affects the layout of the components around it,
4895
+ * we want to make a new context value to ensure they get re-rendered
4896
+ * so they can detect that layout change.
4883
4897
  */
4884
- if (!hasTarget("root", targets)) {
4885
- css.set(":root", {
4886
- "view-transition-name": "none",
4887
- });
4898
+ if (presenceAffectsLayout && isReusedContext) {
4899
+ context = { ...context };
4888
4900
  }
4901
+ React$1.useMemo(() => {
4902
+ presenceChildren.forEach((_, key) => presenceChildren.set(key, false));
4903
+ }, [isPresent]);
4889
4904
  /**
4890
- * Set the timing curve to linear for all view transition layers.
4891
- * This gets baked into the keyframes, which can't be changed
4892
- * without breaking the generated animation.
4893
- *
4894
- * This allows us to set easing via updateTiming - which can be changed.
4905
+ * If there's no `motion` components to fire exit animations, we want to remove this
4906
+ * component immediately.
4895
4907
  */
4896
- css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
4897
- css.commit(); // Write
4898
- const transition = document.startViewTransition(async () => {
4899
- await update();
4900
- // TODO: Go over new targets and ensure they all have ids
4901
- });
4902
- transition.finished.finally(() => {
4903
- css.remove(); // Write
4904
- });
4905
- return new Promise((resolve) => {
4906
- transition.ready.then(() => {
4907
- const generatedViewAnimations = getViewAnimations();
4908
- const animations = [];
4909
- /**
4910
- * Create animations for each of our explicitly-defined subjects.
4911
- */
4912
- targets.forEach((definition, target) => {
4913
- // TODO: If target is not "root", resolve elements
4914
- // and iterate over each
4915
- for (const key of definitionNames) {
4916
- if (!definition[key])
4917
- continue;
4918
- const { keyframes, options } = definition[key];
4919
- for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
4920
- if (!valueKeyframes)
4921
- continue;
4922
- const valueOptions = {
4923
- ...getValueTransition$1(defaultOptions, valueName),
4924
- ...getValueTransition$1(options, valueName),
4925
- };
4926
- const type = chooseLayerType(key);
4927
- /**
4928
- * If this is an opacity animation, and keyframes are not an array,
4929
- * we need to convert them into an array and set an initial value.
4930
- */
4931
- if (valueName === "opacity" &&
4932
- !Array.isArray(valueKeyframes)) {
4933
- const initialValue = type === "new" ? 0 : 1;
4934
- valueKeyframes = [initialValue, valueKeyframes];
4935
- }
4936
- /**
4937
- * Resolve stagger function if provided.
4938
- */
4939
- if (typeof valueOptions.delay === "function") {
4940
- valueOptions.delay = valueOptions.delay(0, 1);
4941
- }
4942
- valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
4943
- valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
4944
- const animation = new NativeAnimation({
4945
- ...valueOptions,
4946
- element: document.documentElement,
4947
- name: valueName,
4948
- pseudoElement: `::view-transition-${type}(${target})`,
4949
- keyframes: valueKeyframes,
4950
- });
4951
- animations.push(animation);
4952
- }
4953
- }
4954
- });
4955
- /**
4956
- * Handle browser generated animations
4957
- */
4958
- for (const animation of generatedViewAnimations) {
4959
- if (animation.playState === "finished")
4960
- continue;
4961
- const { effect } = animation;
4962
- if (!effect || !(effect instanceof KeyframeEffect))
4963
- continue;
4964
- const { pseudoElement } = effect;
4965
- if (!pseudoElement)
4966
- continue;
4967
- const name = getLayerName(pseudoElement);
4968
- if (!name)
4969
- continue;
4970
- const targetDefinition = targets.get(name.layer);
4971
- if (!targetDefinition) {
4972
- /**
4973
- * If transition name is group then update the timing of the animation
4974
- * whereas if it's old or new then we could possibly replace it using
4975
- * the above method.
4976
- */
4977
- const transitionName = name.type === "group" ? "layout" : "";
4978
- let animationTransition = {
4979
- ...getValueTransition$1(defaultOptions, transitionName),
4980
- };
4981
- animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
4982
- animationTransition =
4983
- applyGeneratorOptions(animationTransition);
4984
- const easing = mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration);
4985
- effect.updateTiming({
4986
- delay: secondsToMilliseconds(animationTransition.delay ?? 0),
4987
- duration: animationTransition.duration,
4988
- easing,
4989
- });
4990
- animations.push(new NativeAnimationWrapper(animation));
4991
- }
4992
- else if (hasOpacity(targetDefinition, "enter") &&
4993
- hasOpacity(targetDefinition, "exit") &&
4994
- effect
4995
- .getKeyframes()
4996
- .some((keyframe) => keyframe.mixBlendMode)) {
4997
- animations.push(new NativeAnimationWrapper(animation));
4998
- }
4999
- else {
5000
- animation.cancel();
5001
- }
5002
- }
5003
- resolve(new GroupAnimation(animations));
5004
- });
5005
- });
5006
- }
5007
- function hasOpacity(target, key) {
5008
- return target?.[key]?.keyframes.opacity;
4908
+ React__namespace.useEffect(() => {
4909
+ !isPresent &&
4910
+ !presenceChildren.size &&
4911
+ onExitComplete &&
4912
+ onExitComplete();
4913
+ }, [isPresent]);
4914
+ if (mode === "popLayout") {
4915
+ children = (jsx(PopChild, { isPresent: isPresent, anchorX: anchorX, children: children }));
4916
+ }
4917
+ return (jsx(PresenceContext.Provider, { value: context, children: children }));
4918
+ };
4919
+ function newChildrenMap() {
4920
+ return new Map();
5009
4921
  }
5010
4922
 
5011
- let builders = [];
5012
- let current = null;
5013
- function next() {
5014
- current = null;
5015
- const [nextBuilder] = builders;
5016
- if (nextBuilder)
5017
- start(nextBuilder);
4923
+ /**
4924
+ * When a component is the child of `AnimatePresence`, it can use `usePresence`
4925
+ * to access information about whether it's still present in the React tree.
4926
+ *
4927
+ * ```jsx
4928
+ * import { usePresence } from "framer-motion"
4929
+ *
4930
+ * export const Component = () => {
4931
+ * const [isPresent, safeToRemove] = usePresence()
4932
+ *
4933
+ * useEffect(() => {
4934
+ * !isPresent && setTimeout(safeToRemove, 1000)
4935
+ * }, [isPresent])
4936
+ *
4937
+ * return <div />
4938
+ * }
4939
+ * ```
4940
+ *
4941
+ * If `isPresent` is `false`, it means that a component has been removed the tree, but
4942
+ * `AnimatePresence` won't really remove it until `safeToRemove` has been called.
4943
+ *
4944
+ * @public
4945
+ */
4946
+ function usePresence(subscribe = true) {
4947
+ const context = React$1.useContext(PresenceContext);
4948
+ if (context === null)
4949
+ return [true, null];
4950
+ const { isPresent, onExitComplete, register } = context;
4951
+ // It's safe to call the following hooks conditionally (after an early return) because the context will always
4952
+ // either be null or non-null for the lifespan of the component.
4953
+ const id = React$1.useId();
4954
+ React$1.useEffect(() => {
4955
+ if (subscribe) {
4956
+ return register(id);
4957
+ }
4958
+ }, [subscribe]);
4959
+ const safeToRemove = React$1.useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
4960
+ return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
5018
4961
  }
5019
- function start(builder) {
5020
- removeItem(builders, builder);
5021
- current = builder;
5022
- startViewAnimation(builder).then((animation) => {
5023
- builder.notifyReady(animation);
5024
- animation.finished.finally(next);
4962
+ /**
4963
+ * Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
4964
+ * There is no `safeToRemove` function.
4965
+ *
4966
+ * ```jsx
4967
+ * import { useIsPresent } from "framer-motion"
4968
+ *
4969
+ * export const Component = () => {
4970
+ * const isPresent = useIsPresent()
4971
+ *
4972
+ * useEffect(() => {
4973
+ * !isPresent && console.log("I've been removed!")
4974
+ * }, [isPresent])
4975
+ *
4976
+ * return <div />
4977
+ * }
4978
+ * ```
4979
+ *
4980
+ * @public
4981
+ */
4982
+ function useIsPresent() {
4983
+ return isPresent(React$1.useContext(PresenceContext));
4984
+ }
4985
+ function isPresent(context) {
4986
+ return context === null ? true : context.isPresent;
4987
+ }
4988
+
4989
+ const getChildKey = (child) => child.key || "";
4990
+ function onlyElements(children) {
4991
+ const filtered = [];
4992
+ // We use forEach here instead of map as map mutates the component key by preprending `.$`
4993
+ React$1.Children.forEach(children, (child) => {
4994
+ if (React$1.isValidElement(child))
4995
+ filtered.push(child);
5025
4996
  });
4997
+ return filtered;
5026
4998
  }
5027
- function processQueue() {
4999
+
5000
+ /**
5001
+ * `AnimatePresence` enables the animation of components that have been removed from the tree.
5002
+ *
5003
+ * When adding/removing more than a single child, every child **must** be given a unique `key` prop.
5004
+ *
5005
+ * Any `motion` components that have an `exit` property defined will animate out when removed from
5006
+ * the tree.
5007
+ *
5008
+ * ```jsx
5009
+ * import { motion, AnimatePresence } from 'framer-motion'
5010
+ *
5011
+ * export const Items = ({ items }) => (
5012
+ * <AnimatePresence>
5013
+ * {items.map(item => (
5014
+ * <motion.div
5015
+ * key={item.id}
5016
+ * initial={{ opacity: 0 }}
5017
+ * animate={{ opacity: 1 }}
5018
+ * exit={{ opacity: 0 }}
5019
+ * />
5020
+ * ))}
5021
+ * </AnimatePresence>
5022
+ * )
5023
+ * ```
5024
+ *
5025
+ * You can sequence exit animations throughout a tree using variants.
5026
+ *
5027
+ * If a child contains multiple `motion` components with `exit` props, it will only unmount the child
5028
+ * once all `motion` components have finished animating out. Likewise, any components using
5029
+ * `usePresence` all need to call `safeToRemove`.
5030
+ *
5031
+ * @public
5032
+ */
5033
+ const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", }) => {
5034
+ const [isParentPresent, safeToRemove] = usePresence(propagate);
5028
5035
  /**
5029
- * Iterate backwards over the builders array. We can ignore the
5030
- * "wait" animations. If we have an interrupting animation in the
5031
- * queue then we need to batch all preceeding animations into it.
5032
- * Currently this only batches the update functions but will also
5033
- * need to batch the targets.
5036
+ * Filter any children that aren't ReactElements. We can only track components
5037
+ * between renders with a props.key.
5034
5038
  */
5035
- for (let i = builders.length - 1; i >= 0; i--) {
5036
- const builder = builders[i];
5037
- const { interrupt } = builder.options;
5038
- if (interrupt === "immediate") {
5039
- const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update);
5040
- const remaining = builders.slice(i + 1);
5041
- builder.update = () => {
5042
- batchedUpdates.forEach((update) => update());
5043
- };
5044
- // Put the current builder at the front, followed by any "wait" builders
5045
- builders = [builder, ...remaining];
5046
- break;
5039
+ const presentChildren = React$1.useMemo(() => onlyElements(children), [children]);
5040
+ /**
5041
+ * Track the keys of the currently rendered children. This is used to
5042
+ * determine which children are exiting.
5043
+ */
5044
+ const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
5045
+ /**
5046
+ * If `initial={false}` we only want to pass this to components in the first render.
5047
+ */
5048
+ const isInitialRender = React$1.useRef(true);
5049
+ /**
5050
+ * A ref containing the currently present children. When all exit animations
5051
+ * are complete, we use this to re-render the component with the latest children
5052
+ * *committed* rather than the latest children *rendered*.
5053
+ */
5054
+ const pendingPresentChildren = React$1.useRef(presentChildren);
5055
+ /**
5056
+ * Track which exiting children have finished animating out.
5057
+ */
5058
+ const exitComplete = useConstant(() => new Map());
5059
+ /**
5060
+ * Save children to render as React state. To ensure this component is concurrent-safe,
5061
+ * we check for exiting children via an effect.
5062
+ */
5063
+ const [diffedChildren, setDiffedChildren] = React$1.useState(presentChildren);
5064
+ const [renderedChildren, setRenderedChildren] = React$1.useState(presentChildren);
5065
+ useIsomorphicLayoutEffect(() => {
5066
+ isInitialRender.current = false;
5067
+ pendingPresentChildren.current = presentChildren;
5068
+ /**
5069
+ * Update complete status of exiting children.
5070
+ */
5071
+ for (let i = 0; i < renderedChildren.length; i++) {
5072
+ const key = getChildKey(renderedChildren[i]);
5073
+ if (!presentKeys.includes(key)) {
5074
+ if (exitComplete.get(key) !== true) {
5075
+ exitComplete.set(key, false);
5076
+ }
5077
+ }
5078
+ else {
5079
+ exitComplete.delete(key);
5080
+ }
5047
5081
  }
5048
- }
5049
- if (!current || builders[0]?.options.interrupt === "immediate") {
5050
- next();
5051
- }
5052
- }
5053
- function addToQueue(builder) {
5054
- builders.push(builder);
5055
- microtask.render(processQueue);
5056
- }
5057
-
5058
- class ViewTransitionBuilder {
5059
- constructor(update, options = {}) {
5060
- this.currentTarget = "root";
5061
- this.targets = new Map();
5062
- this.notifyReady = noop;
5063
- this.readyPromise = new Promise((resolve) => {
5064
- this.notifyReady = resolve;
5065
- });
5066
- this.update = update;
5067
- this.options = {
5068
- interrupt: "wait",
5069
- ...options,
5070
- };
5071
- addToQueue(this);
5072
- }
5073
- get(selector) {
5074
- this.currentTarget = selector;
5075
- return this;
5076
- }
5077
- layout(keyframes, options) {
5078
- this.updateTarget("layout", keyframes, options);
5079
- return this;
5080
- }
5081
- new(keyframes, options) {
5082
- this.updateTarget("new", keyframes, options);
5083
- return this;
5084
- }
5085
- old(keyframes, options) {
5086
- this.updateTarget("old", keyframes, options);
5087
- return this;
5088
- }
5089
- enter(keyframes, options) {
5090
- this.updateTarget("enter", keyframes, options);
5091
- return this;
5092
- }
5093
- exit(keyframes, options) {
5094
- this.updateTarget("exit", keyframes, options);
5095
- return this;
5096
- }
5097
- crossfade(options) {
5098
- this.updateTarget("enter", { opacity: 1 }, options);
5099
- this.updateTarget("exit", { opacity: 0 }, options);
5100
- return this;
5101
- }
5102
- updateTarget(target, keyframes, options = {}) {
5103
- const { currentTarget, targets } = this;
5104
- if (!targets.has(currentTarget)) {
5105
- targets.set(currentTarget, {});
5082
+ }, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
5083
+ const exitingChildren = [];
5084
+ if (presentChildren !== diffedChildren) {
5085
+ let nextChildren = [...presentChildren];
5086
+ /**
5087
+ * Loop through all the currently rendered components and decide which
5088
+ * are exiting.
5089
+ */
5090
+ for (let i = 0; i < renderedChildren.length; i++) {
5091
+ const child = renderedChildren[i];
5092
+ const key = getChildKey(child);
5093
+ if (!presentKeys.includes(key)) {
5094
+ nextChildren.splice(i, 0, child);
5095
+ exitingChildren.push(child);
5096
+ }
5106
5097
  }
5107
- const targetData = targets.get(currentTarget);
5108
- targetData[target] = { keyframes, options };
5098
+ /**
5099
+ * If we're in "wait" mode, and we have exiting children, we want to
5100
+ * only render these until they've all exited.
5101
+ */
5102
+ if (mode === "wait" && exitingChildren.length) {
5103
+ nextChildren = exitingChildren;
5104
+ }
5105
+ setRenderedChildren(onlyElements(nextChildren));
5106
+ setDiffedChildren(presentChildren);
5107
+ /**
5108
+ * Early return to ensure once we've set state with the latest diffed
5109
+ * children, we can immediately re-render.
5110
+ */
5111
+ return null;
5109
5112
  }
5110
- then(resolve, reject) {
5111
- return this.readyPromise.then(resolve, reject);
5113
+ if (mode === "wait" &&
5114
+ renderedChildren.length > 1) {
5115
+ console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`);
5112
5116
  }
5113
- }
5114
- function animateView(update, defaultOptions = {}) {
5115
- return new ViewTransitionBuilder(update, defaultOptions);
5116
- }
5117
+ /**
5118
+ * If we've been provided a forceRender function by the LayoutGroupContext,
5119
+ * we can use it to force a re-render amongst all surrounding components once
5120
+ * all components have finished animating out.
5121
+ */
5122
+ const { forceRender } = React$1.useContext(LayoutGroupContext);
5123
+ return (jsx(Fragment, { children: renderedChildren.map((child) => {
5124
+ const key = getChildKey(child);
5125
+ const isPresent = propagate && !isParentPresent
5126
+ ? false
5127
+ : presentChildren === renderedChildren ||
5128
+ presentKeys.includes(key);
5129
+ const onExit = () => {
5130
+ if (exitComplete.has(key)) {
5131
+ exitComplete.set(key, true);
5132
+ }
5133
+ else {
5134
+ return;
5135
+ }
5136
+ let isEveryExitComplete = true;
5137
+ exitComplete.forEach((isExitComplete) => {
5138
+ if (!isExitComplete)
5139
+ isEveryExitComplete = false;
5140
+ });
5141
+ if (isEveryExitComplete) {
5142
+ forceRender?.();
5143
+ setRenderedChildren(pendingPresentChildren.current);
5144
+ propagate && safeToRemove?.();
5145
+ onExitComplete && onExitComplete();
5146
+ }
5147
+ };
5148
+ return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
5149
+ ? undefined
5150
+ : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
5151
+ }) }));
5152
+ };
5117
5153
 
5118
5154
  /**
5119
- * @deprecated
5155
+ * Note: Still used by components generated by old versions of Framer
5120
5156
  *
5121
- * Import as `frame` instead.
5122
- */
5123
- const sync = frame;
5124
- /**
5125
5157
  * @deprecated
5126
- *
5127
- * Use cancelFrame(callback) instead.
5128
5158
  */
5129
- const cancelSync = stepsOrder.reduce((acc, key) => {
5130
- acc[key] = (process) => cancelFrame(process);
5131
- return acc;
5132
- }, {});
5159
+ const DeprecatedLayoutGroupContext = React$1.createContext(null);
5133
5160
 
5134
5161
  const SCALE_PRECISION = 0.0001;
5135
5162
  const SCALE_MIN = 1 - SCALE_PRECISION;
@@ -5369,10 +5396,6 @@
5369
5396
  return visualElement.props[optimizedAppearDataAttribute];
5370
5397
  }
5371
5398
 
5372
- function isSVGElement(element) {
5373
- return element instanceof SVGElement && element.tagName !== "svg";
5374
- }
5375
-
5376
5399
  const compareByDepth = (a, b) => a.depth - b.depth;
5377
5400
 
5378
5401
  class FlatTree {
@@ -6170,7 +6193,7 @@
6170
6193
  mount(instance) {
6171
6194
  if (this.instance)
6172
6195
  return;
6173
- this.isSVG = isSVGElement(instance);
6196
+ this.isSVG = isSVGElement(instance) && !isSVGSVGElement(instance);
6174
6197
  this.instance = instance;
6175
6198
  const { layoutId, layout, visualElement } = this.options;
6176
6199
  if (visualElement && !visualElement.current) {
@@ -11959,7 +11982,7 @@
11959
11982
  latestValues: {},
11960
11983
  },
11961
11984
  };
11962
- const node = isSVGElement(element)
11985
+ const node = isSVGElement(element) && !isSVGSVGElement(element)
11963
11986
  ? new SVGVisualElement(options)
11964
11987
  : new HTMLVisualElement(options);
11965
11988
  node.mount(element);
@@ -12177,7 +12200,7 @@
12177
12200
  const { inlineSize, blockSize } = borderBoxSize[0];
12178
12201
  return { width: inlineSize, height: blockSize };
12179
12202
  }
12180
- else if (target instanceof SVGElement && "getBBox" in target) {
12203
+ else if (isSVGElement(target) && "getBBox" in target) {
12181
12204
  return target.getBBox();
12182
12205
  }
12183
12206
  else {
@@ -12319,7 +12342,7 @@
12319
12342
  const inset = { x: 0, y: 0 };
12320
12343
  let current = element;
12321
12344
  while (current && current !== container) {
12322
- if (current instanceof HTMLElement) {
12345
+ if (isHTMLElement(current)) {
12323
12346
  inset.x += current.offsetLeft;
12324
12347
  inset.y += current.offsetTop;
12325
12348
  current = current.offsetParent;
@@ -13868,11 +13891,15 @@
13868
13891
  exports.isDragging = isDragging;
13869
13892
  exports.isEasingArray = isEasingArray;
13870
13893
  exports.isGenerator = isGenerator;
13894
+ exports.isHTMLElement = isHTMLElement;
13871
13895
  exports.isMotionComponent = isMotionComponent;
13872
13896
  exports.isMotionValue = isMotionValue;
13873
13897
  exports.isNodeOrChild = isNodeOrChild;
13874
13898
  exports.isNumericalString = isNumericalString;
13899
+ exports.isObject = isObject;
13875
13900
  exports.isPrimaryPointer = isPrimaryPointer;
13901
+ exports.isSVGElement = isSVGElement;
13902
+ exports.isSVGSVGElement = isSVGSVGElement;
13876
13903
  exports.isValidMotionProp = isValidMotionProp;
13877
13904
  exports.isWaapiSupportedEasing = isWaapiSupportedEasing;
13878
13905
  exports.isZeroValueString = isZeroValueString;