framer-motion
Version:
A simple and powerful JavaScript animation library
1,377 lines (1,311 loc) • 535 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) :
typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Motion = {}, global.React));
})(this, (function (exports, React$1) { 'use strict';
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React$1);
// source: react/cjs/react-jsx-runtime.production.min.js
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var f = React,
k = Symbol.for("react.element"),
l = Symbol.for("react.fragment"),
m$1 = Object.prototype.hasOwnProperty,
n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,
p = { key: !0, ref: !0, __self: !0, __source: !0 };
function q(c, a, g) {
var b,
d = {},
e = null,
h = null;
void 0 !== g && (e = "" + g);
void 0 !== a.key && (e = "" + a.key);
void 0 !== a.ref && (h = a.ref);
for (b in a) m$1.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
if (c && c.defaultProps)
for (b in ((a = c.defaultProps), a)) void 0 === d[b] && (d[b] = a[b]);
return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current }
}
const Fragment = l;
const jsx = q;
const jsxs = q;
const LayoutGroupContext = React$1.createContext({});
/**
* Creates a constant value over the lifecycle of a component.
*
* Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
* a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
* you can ensure that initialisers don't execute twice or more.
*/
function useConstant(init) {
const ref = React$1.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
const isBrowser = typeof window !== "undefined";
const useIsomorphicLayoutEffect = isBrowser ? React$1.useLayoutEffect : React$1.useEffect;
/**
* @public
*/
const PresenceContext =
/* @__PURE__ */ React$1.createContext(null);
/**
* @public
*/
const MotionConfigContext = React$1.createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
/**
* Measurement functionality has to be within a separate component
* to leverage snapshot lifecycle.
*/
class PopChildMeasure extends React__namespace.Component {
getSnapshotBeforeUpdate(prevProps) {
const element = this.props.childRef.current;
if (element && prevProps.isPresent && !this.props.isPresent) {
const parent = element.offsetParent;
const parentWidth = parent instanceof HTMLElement ? parent.offsetWidth || 0 : 0;
const size = this.props.sizeRef.current;
size.height = element.offsetHeight || 0;
size.width = element.offsetWidth || 0;
size.top = element.offsetTop;
size.left = element.offsetLeft;
size.right = parentWidth - size.width - size.left;
}
return null;
}
/**
* Required with getSnapshotBeforeUpdate to stop React complaining.
*/
componentDidUpdate() { }
render() {
return this.props.children;
}
}
function PopChild({ children, isPresent, anchorX }) {
const id = React$1.useId();
const ref = React$1.useRef(null);
const size = React$1.useRef({
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
});
const { nonce } = React$1.useContext(MotionConfigContext);
/**
* We create and inject a style block so we can apply this explicit
* sizing in a non-destructive manner by just deleting the style block.
*
* We can't apply size via render as the measurement happens
* in getSnapshotBeforeUpdate (post-render), likewise if we apply the
* styles directly on the DOM node, we might be overwriting
* styles set via the style prop.
*/
React$1.useInsertionEffect(() => {
const { width, height, top, left, right } = size.current;
if (isPresent || !ref.current || !width || !height)
return;
const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`;
ref.current.dataset.motionPopId = id;
const style = document.createElement("style");
if (nonce)
style.nonce = nonce;
document.head.appendChild(style);
if (style.sheet) {
style.sheet.insertRule(`
[data-motion-pop-id="${id}"] {
position: absolute !important;
width: ${width}px !important;
height: ${height}px !important;
${x}px !important;
top: ${top}px !important;
}
`);
}
return () => {
document.head.removeChild(style);
};
}, [isPresent]);
return (jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React__namespace.cloneElement(children, { ref }) }));
}
const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, }) => {
const presenceChildren = useConstant(newChildrenMap);
const id = React$1.useId();
const memoizedOnExitComplete = React$1.useCallback((childId) => {
presenceChildren.set(childId, true);
for (const isComplete of presenceChildren.values()) {
if (!isComplete)
return; // can stop searching when any is incomplete
}
onExitComplete && onExitComplete();
}, [presenceChildren, onExitComplete]);
const context = React$1.useMemo(() => ({
id,
initial,
isPresent,
custom,
onExitComplete: memoizedOnExitComplete,
register: (childId) => {
presenceChildren.set(childId, false);
return () => presenceChildren.delete(childId);
},
}),
/**
* If the presence of a child affects the layout of the components around it,
* we want to make a new context value to ensure they get re-rendered
* so they can detect that layout change.
*/
presenceAffectsLayout
? [Math.random(), memoizedOnExitComplete]
: [isPresent, memoizedOnExitComplete]);
React$1.useMemo(() => {
presenceChildren.forEach((_, key) => presenceChildren.set(key, false));
}, [isPresent]);
/**
* If there's no `motion` components to fire exit animations, we want to remove this
* component immediately.
*/
React__namespace.useEffect(() => {
!isPresent &&
!presenceChildren.size &&
onExitComplete &&
onExitComplete();
}, [isPresent]);
if (mode === "popLayout") {
children = (jsx(PopChild, { isPresent: isPresent, anchorX: anchorX, children: children }));
}
return (jsx(PresenceContext.Provider, { value: context, children: children }));
};
function newChildrenMap() {
return new Map();
}
/**
* When a component is the child of `AnimatePresence`, it can use `usePresence`
* to access information about whether it's still present in the React tree.
*
* ```jsx
* import { usePresence } from "framer-motion"
*
* export const Component = () => {
* const [isPresent, safeToRemove] = usePresence()
*
* useEffect(() => {
* !isPresent && setTimeout(safeToRemove, 1000)
* }, [isPresent])
*
* return <div />
* }
* ```
*
* If `isPresent` is `false`, it means that a component has been removed the tree, but
* `AnimatePresence` won't really remove it until `safeToRemove` has been called.
*
* @public
*/
function usePresence(subscribe = true) {
const context = React$1.useContext(PresenceContext);
if (context === null)
return [true, null];
const { isPresent, onExitComplete, register } = context;
// It's safe to call the following hooks conditionally (after an early return) because the context will always
// either be null or non-null for the lifespan of the component.
const id = React$1.useId();
React$1.useEffect(() => {
if (subscribe) {
return register(id);
}
}, [subscribe]);
const safeToRemove = React$1.useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
}
/**
* Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
* There is no `safeToRemove` function.
*
* ```jsx
* import { useIsPresent } from "framer-motion"
*
* export const Component = () => {
* const isPresent = useIsPresent()
*
* useEffect(() => {
* !isPresent && console.log("I've been removed!")
* }, [isPresent])
*
* return <div />
* }
* ```
*
* @public
*/
function useIsPresent() {
return isPresent(React$1.useContext(PresenceContext));
}
function isPresent(context) {
return context === null ? true : context.isPresent;
}
const getChildKey = (child) => child.key || "";
function onlyElements(children) {
const filtered = [];
// We use forEach here instead of map as map mutates the component key by preprending `.$`
React$1.Children.forEach(children, (child) => {
if (React$1.isValidElement(child))
filtered.push(child);
});
return filtered;
}
/**
* `AnimatePresence` enables the animation of components that have been removed from the tree.
*
* When adding/removing more than a single child, every child **must** be given a unique `key` prop.
*
* Any `motion` components that have an `exit` property defined will animate out when removed from
* the tree.
*
* ```jsx
* import { motion, AnimatePresence } from 'framer-motion'
*
* export const Items = ({ items }) => (
* <AnimatePresence>
* {items.map(item => (
* <motion.div
* key={item.id}
* initial={{ opacity: 0 }}
* animate={{ opacity: 1 }}
* exit={{ opacity: 0 }}
* />
* ))}
* </AnimatePresence>
* )
* ```
*
* You can sequence exit animations throughout a tree using variants.
*
* If a child contains multiple `motion` components with `exit` props, it will only unmount the child
* once all `motion` components have finished animating out. Likewise, any components using
* `usePresence` all need to call `safeToRemove`.
*
* @public
*/
const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", }) => {
const [isParentPresent, safeToRemove] = usePresence(propagate);
/**
* Filter any children that aren't ReactElements. We can only track components
* between renders with a props.key.
*/
const presentChildren = React$1.useMemo(() => onlyElements(children), [children]);
/**
* Track the keys of the currently rendered children. This is used to
* determine which children are exiting.
*/
const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
/**
* If `initial={false}` we only want to pass this to components in the first render.
*/
const isInitialRender = React$1.useRef(true);
/**
* A ref containing the currently present children. When all exit animations
* are complete, we use this to re-render the component with the latest children
* *committed* rather than the latest children *rendered*.
*/
const pendingPresentChildren = React$1.useRef(presentChildren);
/**
* Track which exiting children have finished animating out.
*/
const exitComplete = useConstant(() => new Map());
/**
* Save children to render as React state. To ensure this component is concurrent-safe,
* we check for exiting children via an effect.
*/
const [diffedChildren, setDiffedChildren] = React$1.useState(presentChildren);
const [renderedChildren, setRenderedChildren] = React$1.useState(presentChildren);
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false;
pendingPresentChildren.current = presentChildren;
/**
* Update complete status of exiting children.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const key = getChildKey(renderedChildren[i]);
if (!presentKeys.includes(key)) {
if (exitComplete.get(key) !== true) {
exitComplete.set(key, false);
}
}
else {
exitComplete.delete(key);
}
}
}, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
const exitingChildren = [];
if (presentChildren !== diffedChildren) {
let nextChildren = [...presentChildren];
/**
* Loop through all the currently rendered components and decide which
* are exiting.
*/
for (let i = 0; i < renderedChildren.length; i++) {
const child = renderedChildren[i];
const key = getChildKey(child);
if (!presentKeys.includes(key)) {
nextChildren.splice(i, 0, child);
exitingChildren.push(child);
}
}
/**
* If we're in "wait" mode, and we have exiting children, we want to
* only render these until they've all exited.
*/
if (mode === "wait" && exitingChildren.length) {
nextChildren = exitingChildren;
}
setRenderedChildren(onlyElements(nextChildren));
setDiffedChildren(presentChildren);
/**
* Early return to ensure once we've set state with the latest diffed
* children, we can immediately re-render.
*/
return null;
}
if (mode === "wait" &&
renderedChildren.length > 1) {
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.`);
}
/**
* If we've been provided a forceRender function by the LayoutGroupContext,
* we can use it to force a re-render amongst all surrounding components once
* all components have finished animating out.
*/
const { forceRender } = React$1.useContext(LayoutGroupContext);
return (jsx(Fragment, { children: renderedChildren.map((child) => {
const key = getChildKey(child);
const isPresent = propagate && !isParentPresent
? false
: presentChildren === renderedChildren ||
presentKeys.includes(key);
const onExit = () => {
if (exitComplete.has(key)) {
exitComplete.set(key, true);
}
else {
return;
}
let isEveryExitComplete = true;
exitComplete.forEach((isExitComplete) => {
if (!isExitComplete)
isEveryExitComplete = false;
});
if (isEveryExitComplete) {
forceRender?.();
setRenderedChildren(pendingPresentChildren.current);
propagate && safeToRemove?.();
onExitComplete && onExitComplete();
}
};
return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
? undefined
: false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
}) }));
};
/**
* Note: Still used by components generated by old versions of Framer
*
* @deprecated
*/
const DeprecatedLayoutGroupContext = React$1.createContext(null);
function addUniqueItem(arr, item) {
if (arr.indexOf(item) === -1)
arr.push(item);
}
function removeItem(arr, item) {
const index = arr.indexOf(item);
if (index > -1)
arr.splice(index, 1);
}
// Adapted from array-move
function moveItem([...arr], fromIndex, toIndex) {
const startIndex = fromIndex < 0 ? arr.length + fromIndex : fromIndex;
if (startIndex >= 0 && startIndex < arr.length) {
const endIndex = toIndex < 0 ? arr.length + toIndex : toIndex;
const [item] = arr.splice(fromIndex, 1);
arr.splice(endIndex, 0, item);
}
return arr;
}
let warning = () => { };
exports.invariant = () => { };
{
warning = (check, message) => {
if (!check && typeof console !== "undefined") {
console.warn(message);
}
};
exports.invariant = (check, message) => {
if (!check) {
throw new Error(message);
}
};
}
const MotionGlobalConfig = {
skipAnimations: false,
useManualTiming: false,
};
/*#__NO_SIDE_EFFECTS__*/
function memo(callback) {
let result;
return () => {
if (result === undefined)
result = callback();
return result;
};
}
/*#__NO_SIDE_EFFECTS__*/
const noop = (any) => any;
/*
Progress within given range
Given a lower limit and an upper limit, we return the progress
(expressed as a number 0-1) represented by the given value, and
limit that progress to within 0-1.
@param [number]: Lower limit
@param [number]: Upper limit
@param [number]: Value to find progress within given range
@return [number]: Progress of value within range as expressed 0-1
*/
/*#__NO_SIDE_EFFECTS__*/
const progress = (from, to, value) => {
const toFromDifference = to - from;
return toFromDifference === 0 ? 1 : (value - from) / toFromDifference;
};
class SubscriptionManager {
constructor() {
this.subscriptions = [];
}
add(handler) {
addUniqueItem(this.subscriptions, handler);
return () => removeItem(this.subscriptions, handler);
}
notify(a, b, c) {
const numSubscriptions = this.subscriptions.length;
if (!numSubscriptions)
return;
if (numSubscriptions === 1) {
/**
* If there's only a single handler we can just call it without invoking a loop.
*/
this.subscriptions[0](a, b, c);
}
else {
for (let i = 0; i < numSubscriptions; i++) {
/**
* Check whether the handler exists before firing as it's possible
* the subscriptions were modified during this loop running.
*/
const handler = this.subscriptions[i];
handler && handler(a, b, c);
}
}
}
getSize() {
return this.subscriptions.length;
}
clear() {
this.subscriptions.length = 0;
}
}
/**
* Converts seconds to milliseconds
*
* @param seconds - Time in seconds.
* @return milliseconds - Converted time in milliseconds.
*/
/*#__NO_SIDE_EFFECTS__*/
const secondsToMilliseconds = (seconds) => seconds * 1000;
/*#__NO_SIDE_EFFECTS__*/
const millisecondsToSeconds = (milliseconds) => milliseconds / 1000;
/*
Convert velocity into velocity per second
@param [number]: Unit per frame
@param [number]: Frame duration in ms
*/
function velocityPerSecond(velocity, frameDuration) {
return frameDuration ? velocity * (1000 / frameDuration) : 0;
}
const warned = new Set();
function warnOnce(condition, message, element) {
if (condition || warned.has(message))
return;
console.warn(message);
if (element)
console.warn(element);
warned.add(message);
}
const supportsScrollTimeline = /* @__PURE__ */ memo(() => window.ScrollTimeline !== undefined);
class GroupAnimation {
constructor(animations) {
// Bound to accomodate common `return animation.stop` pattern
this.stop = () => this.runAll("stop");
this.animations = animations.filter(Boolean);
}
get finished() {
return Promise.all(this.animations.map((animation) => animation.finished));
}
/**
* TODO: Filter out cancelled or stopped animations before returning
*/
getAll(propName) {
return this.animations[0][propName];
}
setAll(propName, newValue) {
for (let i = 0; i < this.animations.length; i++) {
this.animations[i][propName] = newValue;
}
}
attachTimeline(timeline, fallback) {
const subscriptions = this.animations.map((animation) => {
if (supportsScrollTimeline() && animation.attachTimeline) {
return animation.attachTimeline(timeline);
}
else if (typeof fallback === "function") {
return fallback(animation);
}
});
return () => {
subscriptions.forEach((cancel, i) => {
cancel && cancel();
this.animations[i].stop();
});
};
}
get time() {
return this.getAll("time");
}
set time(time) {
this.setAll("time", time);
}
get speed() {
return this.getAll("speed");
}
set speed(speed) {
this.setAll("speed", speed);
}
get startTime() {
return this.getAll("startTime");
}
get duration() {
let max = 0;
for (let i = 0; i < this.animations.length; i++) {
max = Math.max(max, this.animations[i].duration);
}
return max;
}
runAll(methodName) {
this.animations.forEach((controls) => controls[methodName]());
}
flatten() {
this.runAll("flatten");
}
play() {
this.runAll("play");
}
pause() {
this.runAll("pause");
}
cancel() {
this.runAll("cancel");
}
complete() {
this.runAll("complete");
}
}
class GroupAnimationWithThen extends GroupAnimation {
then(onResolve, _onReject) {
return this.finished.finally(onResolve).then(() => { });
}
}
const isCSSVar = (name) => name.startsWith("--");
const style = {
set: (element, name, value) => {
isCSSVar(name)
? element.style.setProperty(name, value)
: (element.style[name] = value);
},
get: (element, name) => {
return isCSSVar(name)
? element.style.getPropertyValue(name)
: element.style[name];
},
};
const isNotNull$1 = (value) => value !== null;
function getFinalKeyframe$1(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
const resolvedKeyframes = keyframes.filter(isNotNull$1);
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
? 0
: resolvedKeyframes.length - 1;
return !index || finalKeyframe === undefined
? resolvedKeyframes[index]
: finalKeyframe;
}
const supportsPartialKeyframes = /*@__PURE__*/ memo(() => {
try {
document.createElement("div").animate({ opacity: [1] });
}
catch (e) {
return false;
}
return true;
});
const pxValues = new Set([
// Border props
"borderWidth",
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderRadius",
"radius",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomRightRadius",
"borderBottomLeftRadius",
// Positioning props
"width",
"maxWidth",
"height",
"maxHeight",
"top",
"right",
"bottom",
"left",
// Spacing props
"padding",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
"margin",
"marginTop",
"marginRight",
"marginBottom",
"marginLeft",
// Misc
"backgroundPositionX",
"backgroundPositionY",
]);
function hydrateKeyframes(element, name, keyframes, pseudoElement) {
if (!Array.isArray(keyframes)) {
keyframes = [keyframes];
}
for (let i = 0; i < keyframes.length; i++) {
if (keyframes[i] === null) {
keyframes[i] =
i === 0 && !pseudoElement
? style.get(element, name)
: keyframes[i - 1];
}
if (typeof keyframes[i] === "number" && pxValues.has(name)) {
keyframes[i] = keyframes[i] + "px";
}
}
if (!pseudoElement && !supportsPartialKeyframes() && keyframes.length < 2) {
keyframes.unshift(style.get(element, name));
}
return keyframes;
}
const statsBuffer = {
value: null,
addProjectionMetrics: null,
};
const isBezierDefinition = (easing) => Array.isArray(easing) && typeof easing[0] === "number";
/**
* Add the ability for test suites to manually set support flags
* to better test more environments.
*/
const supportsFlags = {};
function memoSupports(callback, supportsFlag) {
const memoized = memo(callback);
return () => supportsFlags[supportsFlag] ?? memoized();
}
const supportsLinearEasing = /*@__PURE__*/ memoSupports(() => {
try {
document
.createElement("div")
.animate({ opacity: 0 }, { easing: "linear(0, 1)" });
}
catch (e) {
return false;
}
return true;
}, "linearEasing");
const generateLinearEasing = (easing, duration, // as milliseconds
resolution = 10 // as milliseconds
) => {
let points = "";
const numPoints = Math.max(Math.round(duration / resolution), 2);
for (let i = 0; i < numPoints; i++) {
points += easing(i / (numPoints - 1)) + ", ";
}
return `linear(${points.substring(0, points.length - 2)})`;
};
const cubicBezierAsString = ([a, b, c, d]) => `cubic-bezier(${a}, ${b}, ${c}, ${d})`;
const supportedWaapiEasing = {
linear: "linear",
ease: "ease",
easeIn: "ease-in",
easeOut: "ease-out",
easeInOut: "ease-in-out",
circIn: /*@__PURE__*/ cubicBezierAsString([0, 0.65, 0.55, 1]),
circOut: /*@__PURE__*/ cubicBezierAsString([0.55, 0, 1, 0.45]),
backIn: /*@__PURE__*/ cubicBezierAsString([0.31, 0.01, 0.66, -0.59]),
backOut: /*@__PURE__*/ cubicBezierAsString([0.33, 1.53, 0.69, 0.99]),
};
function mapEasingToNativeEasing(easing, duration) {
if (!easing) {
return undefined;
}
else if (typeof easing === "function" && supportsLinearEasing()) {
return generateLinearEasing(easing, duration);
}
else if (isBezierDefinition(easing)) {
return cubicBezierAsString(easing);
}
else if (Array.isArray(easing)) {
return easing.map((segmentEasing) => mapEasingToNativeEasing(segmentEasing, duration) ||
supportedWaapiEasing.easeOut);
}
else {
return supportedWaapiEasing[easing];
}
}
function startWaapiAnimation(element, valueName, keyframes, { delay = 0, duration = 300, repeat = 0, repeatType = "loop", ease = "easeInOut", times, } = {}, pseudoElement = undefined) {
const keyframeOptions = {
[valueName]: keyframes,
};
if (times)
keyframeOptions.offset = times;
const easing = mapEasingToNativeEasing(ease, duration);
/**
* If this is an easing array, apply to keyframes, not animation as a whole
*/
if (Array.isArray(easing))
keyframeOptions.easing = easing;
const animation = element.animate(keyframeOptions, {
delay,
duration,
easing: !Array.isArray(easing) ? easing : "linear",
fill: "both",
iterations: repeat + 1,
direction: repeatType === "reverse" ? "alternate" : "normal",
pseudoElement,
});
return animation;
}
function isGenerator(type) {
return typeof type === "function" && "applyToOptions" in type;
}
function applyGeneratorOptions({ type, ...options }) {
if (isGenerator(type)) {
return type.applyToOptions(options);
}
else {
options.duration ?? (options.duration = 300);
options.ease ?? (options.ease = "easeOut");
}
return options;
}
const animationMaps = new WeakMap();
const animationMapKey = (name, pseudoElement) => `${name}:${pseudoElement}`;
function getAnimationMap(element) {
const map = animationMaps.get(element) || new Map();
animationMaps.set(element, map);
return map;
}
/**
* NativeAnimation implements AnimationPlaybackControls for the browser's Web Animations API.
*/
class NativeAnimation {
constructor(options) {
/**
* If we already have an animation, we don't need to instantiate one
* and can just use this as a controls interface.
*/
if ("animation" in options) {
this.animation = options.animation;
return;
}
const { element, name, keyframes: unresolvedKeyframes, pseudoElement, allowFlatten = false, } = options;
let { transition } = options;
this.isPseudoElement = Boolean(pseudoElement);
this.allowFlatten = allowFlatten;
/**
* Stop any existing animations on the element before reading existing keyframes.
*
* TODO: Check for VisualElement before using animation state. This is a fallback
* for mini animate(). Do this when implementing NativeAnimationExtended.
*/
const animationMap = getAnimationMap(element);
const key = animationMapKey(name, pseudoElement || "");
const currentAnimation = animationMap.get(key);
currentAnimation && currentAnimation.stop();
/**
* TODO: If these keyframes aren't correctly hydrated then we want to throw
* run an instant animation.
*/
const keyframes = hydrateKeyframes(element, name, unresolvedKeyframes, pseudoElement);
exports.invariant(typeof transition.type !== "string", `animateMini doesn't support "type" as a string. Did you mean to import { spring } from "motion"?`);
transition = applyGeneratorOptions(transition);
this.animation = startWaapiAnimation(element, name, keyframes, transition, pseudoElement);
if (transition.autoplay === false) {
this.animation.pause();
}
this.removeAnimation = () => animationMap.delete(key);
this.animation.onfinish = () => {
if (!pseudoElement) {
style.set(element, name, getFinalKeyframe$1(keyframes, transition));
this.cancel();
}
};
/**
* TODO: Check for VisualElement before using animation state.
*/
animationMap.set(key, this);
}
play() {
this.animation.play();
}
pause() {
this.animation.pause();
}
complete() {
this.animation.finish();
}
cancel() {
try {
this.animation.cancel();
}
catch (e) { }
this.removeAnimation();
}
stop() {
const { state } = this;
if (state === "idle" || state === "finished") {
return;
}
this.commitStyles();
this.cancel();
}
/**
* WAAPI doesn't natively have any interruption capabilities.
*
* In this method, we commit styles back to the DOM before cancelling
* the animation.
*
* This is designed to be overridden by NativeAnimationExtended, which
* will create a renderless JS animation and sample it twice to calculate
* its current value, "previous" value, and therefore allow
* Motion to also correctly calculate velocity for any subsequent animation
* while deferring the commit until the next animation frame.
*/
commitStyles() {
if (!this.isPseudoElement) {
this.animation.commitStyles?.();
}
}
get duration() {
const duration = this.animation.effect?.getComputedTiming().duration || 0;
return millisecondsToSeconds(Number(duration));
}
get time() {
return millisecondsToSeconds(Number(this.animation.currentTime) || 0);
}
set time(newTime) {
this.animation.currentTime = secondsToMilliseconds(newTime);
}
/**
* The playback speed of the animation.
* 1 = normal speed, 2 = double speed, 0.5 = half speed.
*/
get speed() {
return this.animation.playbackRate;
}
set speed(newSpeed) {
this.animation.playbackRate = newSpeed;
}
get state() {
return this.animation.playState;
}
get startTime() {
return Number(this.animation.startTime);
}
get finished() {
return this.animation.finished;
}
flatten() {
if (this.allowFlatten) {
this.animation.effect?.updateTiming({ easing: "linear" });
}
}
/**
* Attaches a timeline to the animation, for instance the `ScrollTimeline`.
*/
attachTimeline(timeline) {
this.animation.timeline = timeline;
this.animation.onfinish = null;
return noop;
}
/**
* Allows the animation to be awaited.
*
* @deprecated Use `finished` instead.
*/
then(onResolve, onReject) {
return this.finished.then(onResolve).catch(onReject);
}
}
function getValueTransition$1(transition, key) {
return (transition?.[key] ??
transition?.["default"] ??
transition);
}
/**
* Implement a practical max duration for keyframe generation
* to prevent infinite loops
*/
const maxGeneratorDuration = 20000;
function calcGeneratorDuration(generator) {
let duration = 0;
const timeStep = 50;
let state = generator.next(duration);
while (!state.done && duration < maxGeneratorDuration) {
duration += timeStep;
state = generator.next(duration);
}
return duration >= maxGeneratorDuration ? Infinity : duration;
}
/**
* Create a progress => progress easing function from a generator.
*/
function createGeneratorEasing(options, scale = 100, createGenerator) {
const generator = createGenerator({ ...options, keyframes: [0, scale] });
const duration = Math.min(calcGeneratorDuration(generator), maxGeneratorDuration);
return {
type: "keyframes",
ease: (progress) => {
return generator.next(duration * progress).value / scale;
},
duration: millisecondsToSeconds(duration),
};
}
function isWaapiSupportedEasing(easing) {
return Boolean((typeof easing === "function" && supportsLinearEasing()) ||
!easing ||
(typeof easing === "string" &&
(easing in supportedWaapiEasing || supportsLinearEasing())) ||
isBezierDefinition(easing) ||
(Array.isArray(easing) && easing.every(isWaapiSupportedEasing)));
}
function attachTimeline(animation, timeline) {
animation.timeline = timeline;
animation.onfinish = null;
}
const stepsOrder = [
"read", // Read
"resolveKeyframes", // Write/Read/Write/Read
"update", // Compute
"preRender", // Compute
"render", // Write
"postRender", // Compute
];
function createRenderStep(runNextFrame, stepName) {
/**
* We create and reuse two queues, one to queue jobs for the current frame
* and one for the next. We reuse to avoid triggering GC after x frames.
*/
let thisFrame = new Set();
let nextFrame = new Set();
/**
* Track whether we're currently processing jobs in this step. This way
* we can decide whether to schedule new jobs for this frame or next.
*/
let isProcessing = false;
let flushNextFrame = false;
/**
* A set of processes which were marked keepAlive when scheduled.
*/
const toKeepAlive = new WeakSet();
let latestFrameData = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
let numCalls = 0;
function triggerCallback(callback) {
if (toKeepAlive.has(callback)) {
step.schedule(callback);
runNextFrame();
}
numCalls++;
callback(latestFrameData);
}
const step = {
/**
* Schedule a process to run on the next frame.
*/
schedule: (callback, keepAlive = false, immediate = false) => {
const addToCurrentFrame = immediate && isProcessing;
const queue = addToCurrentFrame ? thisFrame : nextFrame;
if (keepAlive)
toKeepAlive.add(callback);
if (!queue.has(callback))
queue.add(callback);
return callback;
},
/**
* Cancel the provided callback from running on the next frame.
*/
cancel: (callback) => {
nextFrame.delete(callback);
toKeepAlive.delete(callback);
},
/**
* Execute all schedule callbacks.
*/
process: (frameData) => {
latestFrameData = frameData;
/**
* If we're already processing we've probably been triggered by a flushSync
* inside an existing process. Instead of executing, mark flushNextFrame
* as true and ensure we flush the following frame at the end of this one.
*/
if (isProcessing) {
flushNextFrame = true;
return;
}
isProcessing = true;
[thisFrame, nextFrame] = [nextFrame, thisFrame];
// Execute this frame
thisFrame.forEach(triggerCallback);
/**
* If we're recording stats then
*/
if (stepName && statsBuffer.value) {
statsBuffer.value.frameloop[stepName].push(numCalls);
}
numCalls = 0;
// Clear the frame so no callbacks remain. This is to avoid
// memory leaks should this render step not run for a while.
thisFrame.clear();
isProcessing = false;
if (flushNextFrame) {
flushNextFrame = false;
step.process(frameData);
}
},
};
return step;
}
const maxElapsed$1 = 40;
function createRenderBatcher(scheduleNextBatch, allowKeepAlive) {
let runNextFrame = false;
let useDefaultElapsed = true;
const state = {
delta: 0.0,
timestamp: 0.0,
isProcessing: false,
};
const flagRunNextFrame = () => (runNextFrame = true);
const steps = stepsOrder.reduce((acc, key) => {
acc[key] = createRenderStep(flagRunNextFrame, allowKeepAlive ? key : undefined);
return acc;
}, {});
const { read, resolveKeyframes, update, preRender, render, postRender } = steps;
const processBatch = () => {
const timestamp = MotionGlobalConfig.useManualTiming
? state.timestamp
: performance.now();
runNextFrame = false;
if (!MotionGlobalConfig.useManualTiming) {
state.delta = useDefaultElapsed
? 1000 / 60
: Math.max(Math.min(timestamp - state.timestamp, maxElapsed$1), 1);
}
state.timestamp = timestamp;
state.isProcessing = true;
// Unrolled render loop for better per-frame performance
read.process(state);
resolveKeyframes.process(state);
update.process(state);
preRender.process(state);
render.process(state);
postRender.process(state);
state.isProcessing = false;
if (runNextFrame && allowKeepAlive) {
useDefaultElapsed = false;
scheduleNextBatch(processBatch);
}
};
const wake = () => {
runNextFrame = true;
useDefaultElapsed = true;
if (!state.isProcessing) {
scheduleNextBatch(processBatch);
}
};
const schedule = stepsOrder.reduce((acc, key) => {
const step = steps[key];
acc[key] = (process, keepAlive = false, immediate = false) => {
if (!runNextFrame)
wake();
return step.schedule(process, keepAlive, immediate);
};
return acc;
}, {});
const cancel = (process) => {
for (let i = 0; i < stepsOrder.length; i++) {
steps[stepsOrder[i]].cancel(process);
}
};
return { schedule, cancel, state, steps };
}
const { schedule: microtask, cancel: cancelMicrotask } =
/* @__PURE__ */ createRenderBatcher(queueMicrotask, false);
const { schedule: frame, cancel: cancelFrame, state: frameData, steps: frameSteps, } = /* @__PURE__ */ createRenderBatcher(typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : noop, true);
let now;
function clearTime() {
now = undefined;
}
/**
* An eventloop-synchronous alternative to performance.now().
*
* Ensures that time measurements remain consistent within a synchronous context.
* Usually calling performance.now() twice within the same synchronous context
* will return different values which isn't useful for animations when we're usually
* trying to sync animations to the same frame.
*/
const time = {
now: () => {
if (now === undefined) {
time.set(frameData.isProcessing || MotionGlobalConfig.useManualTiming
? frameData.timestamp
: performance.now());
}
return now;
},
set: (newTime) => {
now = newTime;
queueMicrotask(clearTime);
},
};
const isDragging = {
x: false,
y: false,
};
function isDragActive() {
return isDragging.x || isDragging.y;
}
function setDragLock(axis) {
if (axis === "x" || axis === "y") {
if (isDragging[axis]) {
return null;
}
else {
isDragging[axis] = true;
return () => {
isDragging[axis] = false;
};
}
}
else {
if (isDragging.x || isDragging.y) {
return null;
}
else {
isDragging.x = isDragging.y = true;
return () => {
isDragging.x = isDragging.y = false;
};
}
}
}
function resolveElements(elementOrSelector, scope, selectorCache) {
if (elementOrSelector instanceof EventTarget) {
return [elementOrSelector];
}
else if (typeof elementOrSelector === "string") {
let root = document;
if (scope) {
root = scope.current;
}
const elements = selectorCache?.[elementOrSelector] ??
root.querySelectorAll(elementOrSelector);
return elements ? Array.from(elements) : [];
}
return Array.from(elementOrSelector);
}
function setupGesture(elementOrSelector, options) {
const elements = resolveElements(elementOrSelector);
const gestureAbortController = new AbortController();
const eventOptions = {
passive: true,
...options,
signal: gestureAbortController.signal,
};
const cancel = () => gestureAbortController.abort();
return [elements, eventOptions, cancel];
}
function isValidHover(event) {
return !(event.pointerType === "touch" || isDragActive());
}
/**
* Create a hover gesture. hover() is different to .addEventListener("pointerenter")
* in that it has an easier syntax, filters out polyfilled touch events, interoperates
* with drag gestures, and automatically removes the "pointerennd" event listener when the hover ends.
*
* @public
*/
function hover(elementOrSelector, onHoverStart, options = {}) {
const [elements, eventOptions, cancel] = setupGesture(elementOrSelector, options);
const onPointerEnter = (enterEvent) => {
if (!isValidHover(enterEvent))
return;
const { target } = enterEvent;
const onHoverEnd = onHoverStart(target, enterEvent);
if (typeof onHoverEnd !== "function" || !target)
return;
const onPointerLeave = (leaveEvent) => {
if (!isValidHover(leaveEvent))
return;
onHoverEnd(leaveEvent);
target.removeEventListener("pointerleave", onPointerLeave);
};
target.addEventListener("pointerleave", onPointerLeave, eventOptions);
};
elements.forEach((element) => {
element.addEventListener("pointerenter", onPointerEnter, eventOptions);