securityos/node_modules/framer-motion/dist/cjs/index.js

6314 lines
240 KiB
JavaScript

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var indexLegacy = require('./index-legacy-87714a68.js');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return 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__*/_interopNamespace(React);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
/**
* @public
*/
const MotionConfigContext = React.createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
const MotionContext = React.createContext({});
/**
* @public
*/
const PresenceContext = React.createContext(null);
const useIsomorphicLayoutEffect = indexLegacy.isBrowser ? React.useLayoutEffect : React.useEffect;
const LazyContext = React.createContext({ strict: false });
function useVisualElement(Component, visualState, props, createVisualElement) {
const { visualElement: parent } = React.useContext(MotionContext);
const lazyContext = React.useContext(LazyContext);
const presenceContext = React.useContext(PresenceContext);
const reducedMotionConfig = React.useContext(MotionConfigContext).reducedMotion;
const visualElementRef = React.useRef();
/**
* If we haven't preloaded a renderer, check to see if we have one lazy-loaded
*/
createVisualElement = createVisualElement || lazyContext.renderer;
if (!visualElementRef.current && createVisualElement) {
visualElementRef.current = createVisualElement(Component, {
visualState,
parent,
props,
presenceContext,
blockInitialAnimation: presenceContext
? presenceContext.initial === false
: false,
reducedMotionConfig,
});
}
const visualElement = visualElementRef.current;
React.useInsertionEffect(() => {
visualElement && visualElement.update(props, presenceContext);
});
/**
* Cache this value as we want to know whether HandoffAppearAnimations
* was present on initial render - it will be deleted after this.
*/
const wantsHandoff = React.useRef(Boolean(props[indexLegacy.optimizedAppearDataAttribute] && !window.HandoffComplete));
useIsomorphicLayoutEffect(() => {
if (!visualElement)
return;
visualElement.render();
/**
* Ideally this function would always run in a useEffect.
*
* However, if we have optimised appear animations to handoff from,
* it needs to happen synchronously to ensure there's no flash of
* incorrect styles in the event of a hydration error.
*
* So if we detect a situtation where optimised appear animations
* are running, we use useLayoutEffect to trigger animations.
*/
if (wantsHandoff.current && visualElement.animationState) {
visualElement.animationState.animateChanges();
}
});
React.useEffect(() => {
if (!visualElement)
return;
visualElement.updateFeatures();
if (!wantsHandoff.current && visualElement.animationState) {
visualElement.animationState.animateChanges();
}
if (wantsHandoff.current) {
wantsHandoff.current = false;
// This ensures all future calls to animateChanges() will run in useEffect
window.HandoffComplete = true;
}
});
return visualElement;
}
/**
* Creates a ref function that, when called, hydrates the provided
* external ref and VisualElement.
*/
function useMotionRef(visualState, visualElement, externalRef) {
return React.useCallback((instance) => {
instance && visualState.mount && visualState.mount(instance);
if (visualElement) {
instance
? visualElement.mount(instance)
: visualElement.unmount();
}
if (externalRef) {
if (typeof externalRef === "function") {
externalRef(instance);
}
else if (indexLegacy.isRefObject(externalRef)) {
externalRef.current = instance;
}
}
},
/**
* Only pass a new ref callback to React if we've received a visual element
* factory. Otherwise we'll be mounting/remounting every time externalRef
* or other dependencies change.
*/
[visualElement]);
}
function getCurrentTreeVariants(props, context) {
if (indexLegacy.isControllingVariants(props)) {
const { initial, animate } = props;
return {
initial: initial === false || indexLegacy.isVariantLabel(initial)
? initial
: undefined,
animate: indexLegacy.isVariantLabel(animate) ? animate : undefined,
};
}
return props.inherit !== false ? context : {};
}
function useCreateMotionContext(props) {
const { initial, animate } = getCurrentTreeVariants(props, React.useContext(MotionContext));
return React.useMemo(() => ({ initial, animate }), [variantLabelsAsDependency(initial), variantLabelsAsDependency(animate)]);
}
function variantLabelsAsDependency(prop) {
return Array.isArray(prop) ? prop.join(" ") : prop;
}
function loadFeatures(features) {
for (const key in features) {
indexLegacy.featureDefinitions[key] = {
...indexLegacy.featureDefinitions[key],
...features[key],
};
}
}
const LayoutGroupContext = React.createContext({});
/**
* Internal, exported only for usage in Framer
*/
const SwitchLayoutGroupContext = React.createContext({});
const motionComponentSymbol = Symbol.for("motionComponentSymbol");
/**
* Create a `motion` component.
*
* This function accepts a Component argument, which can be either a string (ie "div"
* for `motion.div`), or an actual React component.
*
* Alongside this is a config option which provides a way of rendering the provided
* component "offline", or outside the React render cycle.
*/
function createMotionComponent({ preloadedFeatures, createVisualElement, useRender, useVisualState, Component, }) {
preloadedFeatures && loadFeatures(preloadedFeatures);
function MotionComponent(props, externalRef) {
/**
* If we need to measure the element we load this functionality in a
* separate class component in order to gain access to getSnapshotBeforeUpdate.
*/
let MeasureLayout;
const configAndProps = {
...React.useContext(MotionConfigContext),
...props,
layoutId: useLayoutId(props),
};
const { isStatic } = configAndProps;
const context = useCreateMotionContext(props);
const visualState = useVisualState(props, isStatic);
if (!isStatic && indexLegacy.isBrowser) {
/**
* Create a VisualElement for this component. A VisualElement provides a common
* interface to renderer-specific APIs (ie DOM/Three.js etc) as well as
* providing a way of rendering to these APIs outside of the React render loop
* for more performant animations and interactions
*/
context.visualElement = useVisualElement(Component, visualState, configAndProps, createVisualElement);
/**
* Load Motion gesture and animation features. These are rendered as renderless
* components so each feature can optionally make use of React lifecycle methods.
*/
const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext);
const isStrict = React.useContext(LazyContext).strict;
if (context.visualElement) {
MeasureLayout = context.visualElement.loadFeatures(
// Note: Pass the full new combined props to correctly re-render dynamic feature components.
configAndProps, isStrict, preloadedFeatures, initialLayoutGroupConfig);
}
}
/**
* The mount order and hierarchy is specific to ensure our element ref
* is hydrated by the time features fire their effects.
*/
return (React__namespace.createElement(MotionContext.Provider, { value: context },
MeasureLayout && context.visualElement ? (React__namespace.createElement(MeasureLayout, { visualElement: context.visualElement, ...configAndProps })) : null,
useRender(Component, props, useMotionRef(visualState, context.visualElement, externalRef), visualState, isStatic, context.visualElement)));
}
const ForwardRefComponent = React.forwardRef(MotionComponent);
ForwardRefComponent[motionComponentSymbol] = Component;
return ForwardRefComponent;
}
function useLayoutId({ layoutId }) {
const layoutGroupId = React.useContext(LayoutGroupContext).id;
return layoutGroupId && layoutId !== undefined
? layoutGroupId + "-" + layoutId
: layoutId;
}
/**
* Convert any React component into a `motion` component. The provided component
* **must** use `React.forwardRef` to the underlying DOM component you want to animate.
*
* ```jsx
* const Component = React.forwardRef((props, ref) => {
* return <div ref={ref} />
* })
*
* const MotionComponent = motion(Component)
* ```
*
* @public
*/
function createMotionProxy(createConfig) {
function custom(Component, customMotionComponentConfig = {}) {
return createMotionComponent(createConfig(Component, customMotionComponentConfig));
}
if (typeof Proxy === "undefined") {
return custom;
}
/**
* A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc.
* Rather than generating them anew every render.
*/
const componentCache = new Map();
return new Proxy(custom, {
/**
* Called when `motion` is referenced with a prop: `motion.div`, `motion.input` etc.
* The prop name is passed through as `key` and we can use that to generate a `motion`
* DOM component with that name.
*/
get: (_target, key) => {
/**
* If this element doesn't exist in the component cache, create it and cache.
*/
if (!componentCache.has(key)) {
componentCache.set(key, custom(key));
}
return componentCache.get(key);
},
});
}
/**
* We keep these listed seperately as we use the lowercase tag names as part
* of the runtime bundle to detect SVG components
*/
const lowercaseSVGElements = [
"animate",
"circle",
"defs",
"desc",
"ellipse",
"g",
"image",
"line",
"filter",
"marker",
"mask",
"metadata",
"path",
"pattern",
"polygon",
"polyline",
"rect",
"stop",
"switch",
"symbol",
"svg",
"text",
"tspan",
"use",
"view",
];
function isSVGComponent(Component) {
if (
/**
* If it's not a string, it's a custom React component. Currently we only support
* HTML custom React components.
*/
typeof Component !== "string" ||
/**
* If it contains a dash, the element is a custom HTML webcomponent.
*/
Component.includes("-")) {
return false;
}
else if (
/**
* If it's in our list of lowercase SVG tags, it's an SVG component
*/
lowercaseSVGElements.indexOf(Component) > -1 ||
/**
* If it contains a capital letter, it's an SVG component
*/
/[A-Z]/.test(Component)) {
return true;
}
return false;
}
const createHtmlRenderState = () => ({
style: {},
transform: {},
transformOrigin: {},
vars: {},
});
function copyRawValuesOnly(target, source, props) {
for (const key in source) {
if (!indexLegacy.isMotionValue(source[key]) && !indexLegacy.isForcedMotionValue(key, props)) {
target[key] = source[key];
}
}
}
function useInitialMotionValues({ transformTemplate }, visualState, isStatic) {
return React.useMemo(() => {
const state = createHtmlRenderState();
indexLegacy.buildHTMLStyles(state, visualState, { enableHardwareAcceleration: !isStatic }, transformTemplate);
return Object.assign({}, state.vars, state.style);
}, [visualState]);
}
function useStyle(props, visualState, isStatic) {
const styleProp = props.style || {};
const style = {};
/**
* Copy non-Motion Values straight into style
*/
copyRawValuesOnly(style, styleProp, props);
Object.assign(style, useInitialMotionValues(props, visualState, isStatic));
return props.transformValues ? props.transformValues(style) : style;
}
function useHTMLProps(props, visualState, isStatic) {
// The `any` isn't ideal but it is the type of createElement props argument
const htmlProps = {};
const style = useStyle(props, visualState, isStatic);
if (props.drag && props.dragListener !== false) {
// Disable the ghost element when a user drags
htmlProps.draggable = false;
// Disable text selection
style.userSelect =
style.WebkitUserSelect =
style.WebkitTouchCallout =
"none";
// Disable scrolling on the draggable direction
style.touchAction =
props.drag === true
? "none"
: `pan-${props.drag === "x" ? "y" : "x"}`;
}
if (props.tabIndex === undefined &&
(props.onTap || props.onTapStart || props.whileTap)) {
htmlProps.tabIndex = 0;
}
htmlProps.style = style;
return htmlProps;
}
/**
* A list of all valid MotionProps.
*
* @privateRemarks
* This doesn't throw if a `MotionProp` name is missing - it should.
*/
const validMotionProps = new Set([
"animate",
"exit",
"variants",
"initial",
"style",
"values",
"variants",
"transition",
"transformTemplate",
"transformValues",
"custom",
"inherit",
"onBeforeLayoutMeasure",
"onAnimationStart",
"onAnimationComplete",
"onUpdate",
"onDragStart",
"onDrag",
"onDragEnd",
"onMeasureDragConstraints",
"onDirectionLock",
"onDragTransitionEnd",
"_dragX",
"_dragY",
"onHoverStart",
"onHoverEnd",
"onViewportEnter",
"onViewportLeave",
"globalTapTarget",
"ignoreStrict",
"viewport",
]);
/**
* Check whether a prop name is a valid `MotionProp` key.
*
* @param key - Name of the property to check
* @returns `true` is key is a valid `MotionProp`.
*
* @public
*/
function isValidMotionProp(key) {
return (key.startsWith("while") ||
(key.startsWith("drag") && key !== "draggable") ||
key.startsWith("layout") ||
key.startsWith("onTap") ||
key.startsWith("onPan") ||
key.startsWith("onLayout") ||
validMotionProps.has(key));
}
let shouldForward = (key) => !isValidMotionProp(key);
function loadExternalIsValidProp(isValidProp) {
if (!isValidProp)
return;
// Explicitly filter our events
shouldForward = (key) => key.startsWith("on") ? !isValidMotionProp(key) : isValidProp(key);
}
/**
* Emotion and Styled Components both allow users to pass through arbitrary props to their components
* to dynamically generate CSS. They both use the `@emotion/is-prop-valid` package to determine which
* of these should be passed to the underlying DOM node.
*
* However, when styling a Motion component `styled(motion.div)`, both packages pass through *all* props
* as it's seen as an arbitrary component rather than a DOM node. Motion only allows arbitrary props
* passed through the `custom` prop so it doesn't *need* the payload or computational overhead of
* `@emotion/is-prop-valid`, however to fix this problem we need to use it.
*
* By making it an optionalDependency we can offer this functionality only in the situations where it's
* actually required.
*/
try {
/**
* We attempt to import this package but require won't be defined in esm environments, in that case
* isPropValid will have to be provided via `MotionContext`. In a 6.0.0 this should probably be removed
* in favour of explicit injection.
*/
loadExternalIsValidProp(require("@emotion/is-prop-valid").default);
}
catch (_a) {
// We don't need to actually do anything here - the fallback is the existing `isPropValid`.
}
function filterProps(props, isDom, forwardMotionProps) {
const filteredProps = {};
for (const key in props) {
/**
* values is considered a valid prop by Emotion, so if it's present
* this will be rendered out to the DOM unless explicitly filtered.
*
* We check the type as it could be used with the `feColorMatrix`
* element, which we support.
*/
if (key === "values" && typeof props.values === "object")
continue;
if (shouldForward(key) ||
(forwardMotionProps === true && isValidMotionProp(key)) ||
(!isDom && !isValidMotionProp(key)) ||
// If trying to use native HTML drag events, forward drag listeners
(props["draggable"] && key.startsWith("onDrag"))) {
filteredProps[key] = props[key];
}
}
return filteredProps;
}
const createSvgRenderState = () => ({
...createHtmlRenderState(),
attrs: {},
});
function useSVGProps(props, visualState, _isStatic, Component) {
const visualProps = React.useMemo(() => {
const state = createSvgRenderState();
indexLegacy.buildSVGAttrs(state, visualState, { enableHardwareAcceleration: false }, indexLegacy.isSVGTag(Component), props.transformTemplate);
return {
...state.attrs,
style: { ...state.style },
};
}, [visualState]);
if (props.style) {
const rawStyles = {};
copyRawValuesOnly(rawStyles, props.style, props);
visualProps.style = { ...rawStyles, ...visualProps.style };
}
return visualProps;
}
function createUseRender(forwardMotionProps = false) {
const useRender = (Component, props, ref, { latestValues }, isStatic) => {
const useVisualProps = isSVGComponent(Component)
? useSVGProps
: useHTMLProps;
const visualProps = useVisualProps(props, latestValues, isStatic, Component);
const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps);
const elementProps = {
...filteredProps,
...visualProps,
ref,
};
/**
* If component has been handed a motion value as its child,
* memoise its initial value and render that. Subsequent updates
* will be handled by the onChange handler
*/
const { children } = props;
const renderedChildren = React.useMemo(() => (indexLegacy.isMotionValue(children) ? children.get() : children), [children]);
return React.createElement(Component, {
...elementProps,
children: renderedChildren,
});
};
return useRender;
}
/**
* 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.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}
/**
* If the provided value is a MotionValue, this returns the actual value, otherwise just the value itself
*
* TODO: Remove and move to library
*/
function resolveMotionValue(value) {
const unwrappedValue = indexLegacy.isMotionValue(value) ? value.get() : value;
return indexLegacy.isCustomValue(unwrappedValue)
? unwrappedValue.toValue()
: unwrappedValue;
}
function makeState({ scrapeMotionValuesFromProps, createRenderState, onMount, }, props, context, presenceContext) {
const state = {
latestValues: makeLatestValues(props, context, presenceContext, scrapeMotionValuesFromProps),
renderState: createRenderState(),
};
if (onMount) {
state.mount = (instance) => onMount(props, instance, state);
}
return state;
}
const makeUseVisualState = (config) => (props, isStatic) => {
const context = React.useContext(MotionContext);
const presenceContext = React.useContext(PresenceContext);
const make = () => makeState(config, props, context, presenceContext);
return isStatic ? make() : useConstant(make);
};
function makeLatestValues(props, context, presenceContext, scrapeMotionValues) {
const values = {};
const motionValues = scrapeMotionValues(props, {});
for (const key in motionValues) {
values[key] = resolveMotionValue(motionValues[key]);
}
let { initial, animate } = props;
const isControllingVariants = indexLegacy.isControllingVariants(props);
const isVariantNode = indexLegacy.isVariantNode(props);
if (context &&
isVariantNode &&
!isControllingVariants &&
props.inherit !== false) {
if (initial === undefined)
initial = context.initial;
if (animate === undefined)
animate = context.animate;
}
let isInitialAnimationBlocked = presenceContext
? presenceContext.initial === false
: false;
isInitialAnimationBlocked = isInitialAnimationBlocked || initial === false;
const variantToSet = isInitialAnimationBlocked ? animate : initial;
if (variantToSet &&
typeof variantToSet !== "boolean" &&
!indexLegacy.isAnimationControls(variantToSet)) {
const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet];
list.forEach((definition) => {
const resolved = indexLegacy.resolveVariantFromProps(props, definition);
if (!resolved)
return;
const { transitionEnd, transition, ...target } = resolved;
for (const key in target) {
let valueTarget = target[key];
if (Array.isArray(valueTarget)) {
/**
* Take final keyframe if the initial animation is blocked because
* we want to initialise at the end of that blocked animation.
*/
const index = isInitialAnimationBlocked
? valueTarget.length - 1
: 0;
valueTarget = valueTarget[index];
}
if (valueTarget !== null) {
values[key] = valueTarget;
}
}
for (const key in transitionEnd)
values[key] = transitionEnd[key];
});
}
return values;
}
const svgMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: indexLegacy.scrapeMotionValuesFromProps,
createRenderState: createSvgRenderState,
onMount: (props, instance, { renderState, latestValues }) => {
indexLegacy.frame.read(() => {
try {
renderState.dimensions =
typeof instance.getBBox ===
"function"
? instance.getBBox()
: instance.getBoundingClientRect();
}
catch (e) {
// Most likely trying to measure an unrendered element under Firefox
renderState.dimensions = {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
});
indexLegacy.frame.render(() => {
indexLegacy.buildSVGAttrs(renderState, latestValues, { enableHardwareAcceleration: false }, indexLegacy.isSVGTag(instance.tagName), props.transformTemplate);
indexLegacy.renderSVG(instance, renderState);
});
},
}),
};
const htmlMotionConfig = {
useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: indexLegacy.scrapeMotionValuesFromProps$1,
createRenderState: createHtmlRenderState,
}),
};
function createDomMotionConfig(Component, { forwardMotionProps = false }, preloadedFeatures, createVisualElement) {
const baseConfig = isSVGComponent(Component)
? svgMotionConfig
: htmlMotionConfig;
return {
...baseConfig,
preloadedFeatures,
useRender: createUseRender(forwardMotionProps),
createVisualElement,
Component,
};
}
function addDomEvent(target, eventName, handler, options = { passive: true }) {
target.addEventListener(eventName, handler, options);
return () => target.removeEventListener(eventName, handler);
}
const isPrimaryPointer = (event) => {
if (event.pointerType === "mouse") {
return typeof event.button !== "number" || event.button <= 0;
}
else {
/**
* isPrimary is true for all mice buttons, whereas every touch point
* is regarded as its own input. So subsequent concurrent touch points
* will be false.
*
* Specifically match against false here as incomplete versions of
* PointerEvents in very old browser might have it set as undefined.
*/
return event.isPrimary !== false;
}
};
function extractEventInfo(event, pointType = "page") {
return {
point: {
x: event[pointType + "X"],
y: event[pointType + "Y"],
},
};
}
const addPointerInfo = (handler) => {
return (event) => isPrimaryPointer(event) && handler(event, extractEventInfo(event));
};
function addPointerEvent(target, eventName, handler, options) {
return addDomEvent(target, eventName, addPointerInfo(handler), options);
}
function createLock(name) {
let lock = null;
return () => {
const openLock = () => {
lock = null;
};
if (lock === null) {
lock = name;
return openLock;
}
return false;
};
}
const globalHorizontalLock = createLock("dragHorizontal");
const globalVerticalLock = createLock("dragVertical");
function getGlobalLock(drag) {
let lock = false;
if (drag === "y") {
lock = globalVerticalLock();
}
else if (drag === "x") {
lock = globalHorizontalLock();
}
else {
const openHorizontal = globalHorizontalLock();
const openVertical = globalVerticalLock();
if (openHorizontal && openVertical) {
lock = () => {
openHorizontal();
openVertical();
};
}
else {
// Release the locks because we don't use them
if (openHorizontal)
openHorizontal();
if (openVertical)
openVertical();
}
}
return lock;
}
function isDragActive() {
// Check the gesture lock - if we get it, it means no drag gesture is active
// and we can safely fire the tap gesture.
const openGestureLock = getGlobalLock(true);
if (!openGestureLock)
return true;
openGestureLock();
return false;
}
class Feature {
constructor(node) {
this.isMounted = false;
this.node = node;
}
update() { }
}
function addHoverEvent(node, isActive) {
const eventName = "pointer" + (isActive ? "enter" : "leave");
const callbackName = "onHover" + (isActive ? "Start" : "End");
const handleEvent = (event, info) => {
if (event.pointerType === "touch" || isDragActive())
return;
const props = node.getProps();
if (node.animationState && props.whileHover) {
node.animationState.setActive("whileHover", isActive);
}
if (props[callbackName]) {
indexLegacy.frame.update(() => props[callbackName](event, info));
}
};
return addPointerEvent(node.current, eventName, handleEvent, {
passive: !node.getProps()[callbackName],
});
}
class HoverGesture extends Feature {
mount() {
this.unmount = indexLegacy.pipe(addHoverEvent(this.node, true), addHoverEvent(this.node, false));
}
unmount() { }
}
class FocusGesture extends Feature {
constructor() {
super(...arguments);
this.isActive = false;
}
onFocus() {
let isFocusVisible = false;
/**
* If this element doesn't match focus-visible then don't
* apply whileHover. But, if matches throws that focus-visible
* is not a valid selector then in that browser outline styles will be applied
* to the element by default and we want to match that behaviour with whileFocus.
*/
try {
isFocusVisible = this.node.current.matches(":focus-visible");
}
catch (e) {
isFocusVisible = true;
}
if (!isFocusVisible || !this.node.animationState)
return;
this.node.animationState.setActive("whileFocus", true);
this.isActive = true;
}
onBlur() {
if (!this.isActive || !this.node.animationState)
return;
this.node.animationState.setActive("whileFocus", false);
this.isActive = false;
}
mount() {
this.unmount = indexLegacy.pipe(addDomEvent(this.node.current, "focus", () => this.onFocus()), addDomEvent(this.node.current, "blur", () => this.onBlur()));
}
unmount() { }
}
/**
* Recursively traverse up the tree to check whether the provided child node
* is the parent or a descendant of it.
*
* @param parent - Element to find
* @param child - Element to test against parent
*/
const isNodeOrChild = (parent, child) => {
if (!child) {
return false;
}
else if (parent === child) {
return true;
}
else {
return isNodeOrChild(parent, child.parentElement);
}
};
function fireSyntheticPointerEvent(name, handler) {
if (!handler)
return;
const syntheticPointerEvent = new PointerEvent("pointer" + name);
handler(syntheticPointerEvent, extractEventInfo(syntheticPointerEvent));
}
class PressGesture extends Feature {
constructor() {
super(...arguments);
this.removeStartListeners = indexLegacy.noop;
this.removeEndListeners = indexLegacy.noop;
this.removeAccessibleListeners = indexLegacy.noop;
this.startPointerPress = (startEvent, startInfo) => {
if (this.isPressing)
return;
this.removeEndListeners();
const props = this.node.getProps();
const endPointerPress = (endEvent, endInfo) => {
if (!this.checkPressEnd())
return;
const { onTap, onTapCancel, globalTapTarget } = this.node.getProps();
indexLegacy.frame.update(() => {
/**
* We only count this as a tap gesture if the event.target is the same
* as, or a child of, this component's element
*/
!globalTapTarget &&
!isNodeOrChild(this.node.current, endEvent.target)
? onTapCancel && onTapCancel(endEvent, endInfo)
: onTap && onTap(endEvent, endInfo);
});
};
const removePointerUpListener = addPointerEvent(window, "pointerup", endPointerPress, { passive: !(props.onTap || props["onPointerUp"]) });
const removePointerCancelListener = addPointerEvent(window, "pointercancel", (cancelEvent, cancelInfo) => this.cancelPress(cancelEvent, cancelInfo), { passive: !(props.onTapCancel || props["onPointerCancel"]) });
this.removeEndListeners = indexLegacy.pipe(removePointerUpListener, removePointerCancelListener);
this.startPress(startEvent, startInfo);
};
this.startAccessiblePress = () => {
const handleKeydown = (keydownEvent) => {
if (keydownEvent.key !== "Enter" || this.isPressing)
return;
const handleKeyup = (keyupEvent) => {
if (keyupEvent.key !== "Enter" || !this.checkPressEnd())
return;
fireSyntheticPointerEvent("up", (event, info) => {
const { onTap } = this.node.getProps();
if (onTap) {
indexLegacy.frame.update(() => onTap(event, info));
}
});
};
this.removeEndListeners();
this.removeEndListeners = addDomEvent(this.node.current, "keyup", handleKeyup);
fireSyntheticPointerEvent("down", (event, info) => {
this.startPress(event, info);
});
};
const removeKeydownListener = addDomEvent(this.node.current, "keydown", handleKeydown);
const handleBlur = () => {
if (!this.isPressing)
return;
fireSyntheticPointerEvent("cancel", (cancelEvent, cancelInfo) => this.cancelPress(cancelEvent, cancelInfo));
};
const removeBlurListener = addDomEvent(this.node.current, "blur", handleBlur);
this.removeAccessibleListeners = indexLegacy.pipe(removeKeydownListener, removeBlurListener);
};
}
startPress(event, info) {
this.isPressing = true;
const { onTapStart, whileTap } = this.node.getProps();
/**
* Ensure we trigger animations before firing event callback
*/
if (whileTap && this.node.animationState) {
this.node.animationState.setActive("whileTap", true);
}
if (onTapStart) {
indexLegacy.frame.update(() => onTapStart(event, info));
}
}
checkPressEnd() {
this.removeEndListeners();
this.isPressing = false;
const props = this.node.getProps();
if (props.whileTap && this.node.animationState) {
this.node.animationState.setActive("whileTap", false);
}
return !isDragActive();
}
cancelPress(event, info) {
if (!this.checkPressEnd())
return;
const { onTapCancel } = this.node.getProps();
if (onTapCancel) {
indexLegacy.frame.update(() => onTapCancel(event, info));
}
}
mount() {
const props = this.node.getProps();
const removePointerListener = addPointerEvent(props.globalTapTarget ? window : this.node.current, "pointerdown", this.startPointerPress, { passive: !(props.onTapStart || props["onPointerStart"]) });
const removeFocusListener = addDomEvent(this.node.current, "focus", this.startAccessiblePress);
this.removeStartListeners = indexLegacy.pipe(removePointerListener, removeFocusListener);
}
unmount() {
this.removeStartListeners();
this.removeEndListeners();
this.removeAccessibleListeners();
}
}
/**
* Map an IntersectionHandler callback to an element. We only ever make one handler for one
* element, so even though these handlers might all be triggered by different
* observers, we can keep them in the same map.
*/
const observerCallbacks = new WeakMap();
/**
* Multiple observers can be created for multiple element/document roots. Each with
* different settings. So here we store dictionaries of observers to each root,
* using serialised settings (threshold/margin) as lookup keys.
*/
const observers = new WeakMap();
const fireObserverCallback = (entry) => {
const callback = observerCallbacks.get(entry.target);
callback && callback(entry);
};
const fireAllObserverCallbacks = (entries) => {
entries.forEach(fireObserverCallback);
};
function initIntersectionObserver({ root, ...options }) {
const lookupRoot = root || document;
/**
* If we don't have an observer lookup map for this root, create one.
*/
if (!observers.has(lookupRoot)) {
observers.set(lookupRoot, {});
}
const rootObservers = observers.get(lookupRoot);
const key = JSON.stringify(options);
/**
* If we don't have an observer for this combination of root and settings,
* create one.
*/
if (!rootObservers[key]) {
rootObservers[key] = new IntersectionObserver(fireAllObserverCallbacks, { root, ...options });
}
return rootObservers[key];
}
function observeIntersection(element, options, callback) {
const rootInteresectionObserver = initIntersectionObserver(options);
observerCallbacks.set(element, callback);
rootInteresectionObserver.observe(element);
return () => {
observerCallbacks.delete(element);
rootInteresectionObserver.unobserve(element);
};
}
const thresholdNames = {
some: 0,
all: 1,
};
class InViewFeature extends Feature {
constructor() {
super(...arguments);
this.hasEnteredView = false;
this.isInView = false;
}
startObserver() {
this.unmount();
const { viewport = {} } = this.node.getProps();
const { root, margin: rootMargin, amount = "some", once } = viewport;
const options = {
root: root ? root.current : undefined,
rootMargin,
threshold: typeof amount === "number" ? amount : thresholdNames[amount],
};
const onIntersectionUpdate = (entry) => {
const { isIntersecting } = entry;
/**
* If there's been no change in the viewport state, early return.
*/
if (this.isInView === isIntersecting)
return;
this.isInView = isIntersecting;
/**
* Handle hasEnteredView. If this is only meant to run once, and
* element isn't visible, early return. Otherwise set hasEnteredView to true.
*/
if (once && !isIntersecting && this.hasEnteredView) {
return;
}
else if (isIntersecting) {
this.hasEnteredView = true;
}
if (this.node.animationState) {
this.node.animationState.setActive("whileInView", isIntersecting);
}
/**
* Use the latest committed props rather than the ones in scope
* when this observer is created
*/
const { onViewportEnter, onViewportLeave } = this.node.getProps();
const callback = isIntersecting ? onViewportEnter : onViewportLeave;
callback && callback(entry);
};
return observeIntersection(this.node.current, options, onIntersectionUpdate);
}
mount() {
this.startObserver();
}
update() {
if (typeof IntersectionObserver === "undefined")
return;
const { props, prevProps } = this.node;
const hasOptionsChanged = ["amount", "margin", "root"].some(hasViewportOptionChanged(props, prevProps));
if (hasOptionsChanged) {
this.startObserver();
}
}
unmount() { }
}
function hasViewportOptionChanged({ viewport = {} }, { viewport: prevViewport = {} } = {}) {
return (name) => viewport[name] !== prevViewport[name];
}
const gestureAnimations = {
inView: {
Feature: InViewFeature,
},
tap: {
Feature: PressGesture,
},
focus: {
Feature: FocusGesture,
},
hover: {
Feature: HoverGesture,
},
};
function shallowCompare(next, prev) {
if (!Array.isArray(prev))
return false;
const prevLength = prev.length;
if (prevLength !== next.length)
return false;
for (let i = 0; i < prevLength; i++) {
if (prev[i] !== next[i])
return false;
}
return true;
}
function animateVariant(visualElement, variant, options = {}) {
const resolved = indexLegacy.resolveVariant(visualElement, variant, options.custom);
let { transition = visualElement.getDefaultTransition() || {} } = resolved || {};
if (options.transitionOverride) {
transition = options.transitionOverride;
}
/**
* If we have a variant, create a callback that runs it as an animation.
* Otherwise, we resolve a Promise immediately for a composable no-op.
*/
const getAnimation = resolved
? () => Promise.all(indexLegacy.animateTarget(visualElement, resolved, options))
: () => Promise.resolve();
/**
* If we have children, create a callback that runs all their animations.
* Otherwise, we resolve a Promise immediately for a composable no-op.
*/
const getChildAnimations = visualElement.variantChildren && visualElement.variantChildren.size
? (forwardDelay = 0) => {
const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
return animateChildren(visualElement, variant, delayChildren + forwardDelay, staggerChildren, staggerDirection, options);
}
: () => Promise.resolve();
/**
* If the transition explicitly defines a "when" option, we need to resolve either
* this animation or all children animations before playing the other.
*/
const { when } = transition;
if (when) {
const [first, last] = when === "beforeChildren"
? [getAnimation, getChildAnimations]
: [getChildAnimations, getAnimation];
return first().then(() => last());
}
else {
return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
}
}
function animateChildren(visualElement, variant, delayChildren = 0, staggerChildren = 0, staggerDirection = 1, options) {
const animations = [];
const maxStaggerDuration = (visualElement.variantChildren.size - 1) * staggerChildren;
const generateStaggerDuration = staggerDirection === 1
? (i = 0) => i * staggerChildren
: (i = 0) => maxStaggerDuration - i * staggerChildren;
Array.from(visualElement.variantChildren)
.sort(sortByTreeOrder)
.forEach((child, i) => {
child.notify("AnimationStart", variant);
animations.push(animateVariant(child, variant, {
...options,
delay: delayChildren + generateStaggerDuration(i),
}).then(() => child.notify("AnimationComplete", variant)));
});
return Promise.all(animations);
}
function sortByTreeOrder(a, b) {
return a.sortNodePosition(b);
}
function animateVisualElement(visualElement, definition, options = {}) {
visualElement.notify("AnimationStart", definition);
let animation;
if (Array.isArray(definition)) {
const animations = definition.map((variant) => animateVariant(visualElement, variant, options));
animation = Promise.all(animations);
}
else if (typeof definition === "string") {
animation = animateVariant(visualElement, definition, options);
}
else {
const resolvedDefinition = typeof definition === "function"
? indexLegacy.resolveVariant(visualElement, definition, options.custom)
: definition;
animation = Promise.all(indexLegacy.animateTarget(visualElement, resolvedDefinition, options));
}
return animation.then(() => visualElement.notify("AnimationComplete", definition));
}
const reversePriorityOrder = [...indexLegacy.variantPriorityOrder].reverse();
const numAnimationTypes = indexLegacy.variantPriorityOrder.length;
function animateList(visualElement) {
return (animations) => Promise.all(animations.map(({ animation, options }) => animateVisualElement(visualElement, animation, options)));
}
function createAnimationState(visualElement) {
let animate = animateList(visualElement);
const state = createState();
let isInitialRender = true;
/**
* This function will be used to reduce the animation definitions for
* each active animation type into an object of resolved values for it.
*/
const buildResolvedTypeValues = (acc, definition) => {
const resolved = indexLegacy.resolveVariant(visualElement, definition);
if (resolved) {
const { transition, transitionEnd, ...target } = resolved;
acc = { ...acc, ...target, ...transitionEnd };
}
return acc;
};
/**
* This just allows us to inject mocked animation functions
* @internal
*/
function setAnimateFunction(makeAnimator) {
animate = makeAnimator(visualElement);
}
/**
* When we receive new props, we need to:
* 1. Create a list of protected keys for each type. This is a directory of
* value keys that are currently being "handled" by types of a higher priority
* so that whenever an animation is played of a given type, these values are
* protected from being animated.
* 2. Determine if an animation type needs animating.
* 3. Determine if any values have been removed from a type and figure out
* what to animate those to.
*/
function animateChanges(options, changedActiveType) {
const props = visualElement.getProps();
const context = visualElement.getVariantContext(true) || {};
/**
* A list of animations that we'll build into as we iterate through the animation
* types. This will get executed at the end of the function.
*/
const animations = [];
/**
* Keep track of which values have been removed. Then, as we hit lower priority
* animation types, we can check if they contain removed values and animate to that.
*/
const removedKeys = new Set();
/**
* A dictionary of all encountered keys. This is an object to let us build into and
* copy it without iteration. Each time we hit an animation type we set its protected
* keys - the keys its not allowed to animate - to the latest version of this object.
*/
let encounteredKeys = {};
/**
* If a variant has been removed at a given index, and this component is controlling
* variant animations, we want to ensure lower-priority variants are forced to animate.
*/
let removedVariantIndex = Infinity;
/**
* Iterate through all animation types in reverse priority order. For each, we want to
* detect which values it's handling and whether or not they've changed (and therefore
* need to be animated). If any values have been removed, we want to detect those in
* lower priority props and flag for animation.
*/
for (let i = 0; i < numAnimationTypes; i++) {
const type = reversePriorityOrder[i];
const typeState = state[type];
const prop = props[type] !== undefined ? props[type] : context[type];
const propIsVariant = indexLegacy.isVariantLabel(prop);
/**
* If this type has *just* changed isActive status, set activeDelta
* to that status. Otherwise set to null.
*/
const activeDelta = type === changedActiveType ? typeState.isActive : null;
if (activeDelta === false)
removedVariantIndex = i;
/**
* If this prop is an inherited variant, rather than been set directly on the
* component itself, we want to make sure we allow the parent to trigger animations.
*
* TODO: Can probably change this to a !isControllingVariants check
*/
let isInherited = prop === context[type] && prop !== props[type] && propIsVariant;
/**
*
*/
if (isInherited &&
isInitialRender &&
visualElement.manuallyAnimateOnMount) {
isInherited = false;
}
/**
* Set all encountered keys so far as the protected keys for this type. This will
* be any key that has been animated or otherwise handled by active, higher-priortiy types.
*/
typeState.protectedKeys = { ...encounteredKeys };
// Check if we can skip analysing this prop early
if (
// If it isn't active and hasn't *just* been set as inactive
(!typeState.isActive && activeDelta === null) ||
// If we didn't and don't have any defined prop for this animation type
(!prop && !typeState.prevProp) ||
// Or if the prop doesn't define an animation
indexLegacy.isAnimationControls(prop) ||
typeof prop === "boolean") {
continue;
}
/**
* As we go look through the values defined on this type, if we detect
* a changed value or a value that was removed in a higher priority, we set
* this to true and add this prop to the animation list.
*/
const variantDidChange = checkVariantsDidChange(typeState.prevProp, prop);
let shouldAnimateType = variantDidChange ||
// If we're making this variant active, we want to always make it active
(type === changedActiveType &&
typeState.isActive &&
!isInherited &&
propIsVariant) ||
// If we removed a higher-priority variant (i is in reverse order)
(i > removedVariantIndex && propIsVariant);
let handledRemovedValues = false;
/**
* As animations can be set as variant lists, variants or target objects, we
* coerce everything to an array if it isn't one already
*/
const definitionList = Array.isArray(prop) ? prop : [prop];
/**
* Build an object of all the resolved values. We'll use this in the subsequent
* animateChanges calls to determine whether a value has changed.
*/
let resolvedValues = definitionList.reduce(buildResolvedTypeValues, {});
if (activeDelta === false)
resolvedValues = {};
/**
* Now we need to loop through all the keys in the prev prop and this prop,
* and decide:
* 1. If the value has changed, and needs animating
* 2. If it has been removed, and needs adding to the removedKeys set
* 3. If it has been removed in a higher priority type and needs animating
* 4. If it hasn't been removed in a higher priority but hasn't changed, and
* needs adding to the type's protectedKeys list.
*/
const { prevResolvedValues = {} } = typeState;
const allKeys = {
...prevResolvedValues,
...resolvedValues,
};
const markToAnimate = (key) => {
shouldAnimateType = true;
if (removedKeys.has(key)) {
handledRemovedValues = true;
removedKeys.delete(key);
}
typeState.needsAnimating[key] = true;
};
for (const key in allKeys) {
const next = resolvedValues[key];
const prev = prevResolvedValues[key];
// If we've already handled this we can just skip ahead
if (encounteredKeys.hasOwnProperty(key))
continue;
/**
* If the value has changed, we probably want to animate it.
*/
let valueHasChanged = false;
if (indexLegacy.isKeyframesTarget(next) && indexLegacy.isKeyframesTarget(prev)) {
valueHasChanged = !shallowCompare(next, prev);
}
else {
valueHasChanged = next !== prev;
}
if (valueHasChanged) {
if (next !== undefined) {
// If next is defined and doesn't equal prev, it needs animating
markToAnimate(key);
}
else {
// If it's undefined, it's been removed.
removedKeys.add(key);
}
}
else if (next !== undefined && removedKeys.has(key)) {
/**
* If next hasn't changed and it isn't undefined, we want to check if it's
* been removed by a higher priority
*/
markToAnimate(key);
}
else {
/**
* If it hasn't changed, we add it to the list of protected values
* to ensure it doesn't get animated.
*/
typeState.protectedKeys[key] = true;
}
}
/**
* Update the typeState so next time animateChanges is called we can compare the
* latest prop and resolvedValues to these.
*/
typeState.prevProp = prop;
typeState.prevResolvedValues = resolvedValues;
/**
*
*/
if (typeState.isActive) {
encounteredKeys = { ...encounteredKeys, ...resolvedValues };
}
if (isInitialRender && visualElement.blockInitialAnimation) {
shouldAnimateType = false;
}
/**
* If this is an inherited prop we want to hard-block animations
*/
if (shouldAnimateType && (!isInherited || handledRemovedValues)) {
animations.push(...definitionList.map((animation) => ({
animation: animation,
options: { type, ...options },
})));
}
}
/**
* If there are some removed value that haven't been dealt with,
* we need to create a new animation that falls back either to the value
* defined in the style prop, or the last read value.
*/
if (removedKeys.size) {
const fallbackAnimation = {};
removedKeys.forEach((key) => {
const fallbackTarget = visualElement.getBaseTarget(key);
if (fallbackTarget !== undefined) {
fallbackAnimation[key] = fallbackTarget;
}
});
animations.push({ animation: fallbackAnimation });
}
let shouldAnimate = Boolean(animations.length);
if (isInitialRender &&
(props.initial === false || props.initial === props.animate) &&
!visualElement.manuallyAnimateOnMount) {
shouldAnimate = false;
}
isInitialRender = false;
return shouldAnimate ? animate(animations) : Promise.resolve();
}
/**
* Change whether a certain animation type is active.
*/
function setActive(type, isActive, options) {
var _a;
// If the active state hasn't changed, we can safely do nothing here
if (state[type].isActive === isActive)
return Promise.resolve();
// Propagate active change to children
(_a = visualElement.variantChildren) === null || _a === void 0 ? void 0 : _a.forEach((child) => { var _a; return (_a = child.animationState) === null || _a === void 0 ? void 0 : _a.setActive(type, isActive); });
state[type].isActive = isActive;
const animations = animateChanges(options, type);
for (const key in state) {
state[key].protectedKeys = {};
}
return animations;
}
return {
animateChanges,
setActive,
setAnimateFunction,
getState: () => state,
};
}
function checkVariantsDidChange(prev, next) {
if (typeof next === "string") {
return next !== prev;
}
else if (Array.isArray(next)) {
return !shallowCompare(next, prev);
}
return false;
}
function createTypeState(isActive = false) {
return {
isActive,
protectedKeys: {},
needsAnimating: {},
prevResolvedValues: {},
};
}
function createState() {
return {
animate: createTypeState(true),
whileInView: createTypeState(),
whileHover: createTypeState(),
whileTap: createTypeState(),
whileDrag: createTypeState(),
whileFocus: createTypeState(),
exit: createTypeState(),
};
}
class AnimationFeature extends Feature {
/**
* We dynamically generate the AnimationState manager as it contains a reference
* to the underlying animation library. We only want to load that if we load this,
* so people can optionally code split it out using the `m` component.
*/
constructor(node) {
super(node);
node.animationState || (node.animationState = createAnimationState(node));
}
updateAnimationControlsSubscription() {
const { animate } = this.node.getProps();
this.unmount();
if (indexLegacy.isAnimationControls(animate)) {
this.unmount = animate.subscribe(this.node);
}
}
/**
* Subscribe any provided AnimationControls to the component's VisualElement
*/
mount() {
this.updateAnimationControlsSubscription();
}
update() {
const { animate } = this.node.getProps();
const { animate: prevAnimate } = this.node.prevProps || {};
if (animate !== prevAnimate) {
this.updateAnimationControlsSubscription();
}
}
unmount() { }
}
let id$2 = 0;
class ExitAnimationFeature extends Feature {
constructor() {
super(...arguments);
this.id = id$2++;
}
update() {
if (!this.node.presenceContext)
return;
const { isPresent, onExitComplete, custom } = this.node.presenceContext;
const { isPresent: prevIsPresent } = this.node.prevPresenceContext || {};
if (!this.node.animationState || isPresent === prevIsPresent) {
return;
}
const exitAnimation = this.node.animationState.setActive("exit", !isPresent, { custom: custom !== null && custom !== void 0 ? custom : this.node.getProps().custom });
if (onExitComplete && !isPresent) {
exitAnimation.then(() => onExitComplete(this.id));
}
}
mount() {
const { register } = this.node.presenceContext || {};
if (register) {
this.unmount = register(this.id);
}
}
unmount() { }
}
const animations = {
animation: {
Feature: AnimationFeature,
},
exit: {
Feature: ExitAnimationFeature,
},
};
/**
* @internal
*/
class PanSession {
constructor(event, handlers, { transformPagePoint, contextWindow, dragSnapToOrigin = false } = {}) {
/**
* @internal
*/
this.startEvent = null;
/**
* @internal
*/
this.lastMoveEvent = null;
/**
* @internal
*/
this.lastMoveEventInfo = null;
/**
* @internal
*/
this.handlers = {};
/**
* @internal
*/
this.contextWindow = window;
this.updatePoint = () => {
if (!(this.lastMoveEvent && this.lastMoveEventInfo))
return;
const info = getPanInfo(this.lastMoveEventInfo, this.history);
const isPanStarted = this.startEvent !== null;
// Only start panning if the offset is larger than 3 pixels. If we make it
// any larger than this we'll want to reset the pointer history
// on the first update to avoid visual snapping to the cursoe.
const isDistancePastThreshold = indexLegacy.distance2D(info.offset, { x: 0, y: 0 }) >= 3;
if (!isPanStarted && !isDistancePastThreshold)
return;
const { point } = info;
const { timestamp } = indexLegacy.frameData;
this.history.push({ ...point, timestamp });
const { onStart, onMove } = this.handlers;
if (!isPanStarted) {
onStart && onStart(this.lastMoveEvent, info);
this.startEvent = this.lastMoveEvent;
}
onMove && onMove(this.lastMoveEvent, info);
};
this.handlePointerMove = (event, info) => {
this.lastMoveEvent = event;
this.lastMoveEventInfo = transformPoint(info, this.transformPagePoint);
// Throttle mouse move event to once per frame
indexLegacy.frame.update(this.updatePoint, true);
};
this.handlePointerUp = (event, info) => {
this.end();
const { onEnd, onSessionEnd, resumeAnimation } = this.handlers;
if (this.dragSnapToOrigin)
resumeAnimation && resumeAnimation();
if (!(this.lastMoveEvent && this.lastMoveEventInfo))
return;
const panInfo = getPanInfo(event.type === "pointercancel"
? this.lastMoveEventInfo
: transformPoint(info, this.transformPagePoint), this.history);
if (this.startEvent && onEnd) {
onEnd(event, panInfo);
}
onSessionEnd && onSessionEnd(event, panInfo);
};
// If we have more than one touch, don't start detecting this gesture
if (!isPrimaryPointer(event))
return;
this.dragSnapToOrigin = dragSnapToOrigin;
this.handlers = handlers;
this.transformPagePoint = transformPagePoint;
this.contextWindow = contextWindow || window;
const info = extractEventInfo(event);
const initialInfo = transformPoint(info, this.transformPagePoint);
const { point } = initialInfo;
const { timestamp } = indexLegacy.frameData;
this.history = [{ ...point, timestamp }];
const { onSessionStart } = handlers;
onSessionStart &&
onSessionStart(event, getPanInfo(initialInfo, this.history));
this.removeListeners = indexLegacy.pipe(addPointerEvent(this.contextWindow, "pointermove", this.handlePointerMove), addPointerEvent(this.contextWindow, "pointerup", this.handlePointerUp), addPointerEvent(this.contextWindow, "pointercancel", this.handlePointerUp));
}
updateHandlers(handlers) {
this.handlers = handlers;
}
end() {
this.removeListeners && this.removeListeners();
indexLegacy.cancelFrame(this.updatePoint);
}
}
function transformPoint(info, transformPagePoint) {
return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
}
function subtractPoint(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
function getPanInfo({ point }, history) {
return {
point,
delta: subtractPoint(point, lastDevicePoint(history)),
offset: subtractPoint(point, startDevicePoint(history)),
velocity: getVelocity(history, 0.1),
};
}
function startDevicePoint(history) {
return history[0];
}
function lastDevicePoint(history) {
return history[history.length - 1];
}
function getVelocity(history, timeDelta) {
if (history.length < 2) {
return { x: 0, y: 0 };
}
let i = history.length - 1;
let timestampedPoint = null;
const lastPoint = lastDevicePoint(history);
while (i >= 0) {
timestampedPoint = history[i];
if (lastPoint.timestamp - timestampedPoint.timestamp >
indexLegacy.secondsToMilliseconds(timeDelta)) {
break;
}
i--;
}
if (!timestampedPoint) {
return { x: 0, y: 0 };
}
const time = indexLegacy.millisecondsToSeconds(lastPoint.timestamp - timestampedPoint.timestamp);
if (time === 0) {
return { x: 0, y: 0 };
}
const currentVelocity = {
x: (lastPoint.x - timestampedPoint.x) / time,
y: (lastPoint.y - timestampedPoint.y) / time,
};
if (currentVelocity.x === Infinity) {
currentVelocity.x = 0;
}
if (currentVelocity.y === Infinity) {
currentVelocity.y = 0;
}
return currentVelocity;
}
function calcLength(axis) {
return axis.max - axis.min;
}
function isNear(value, target = 0, maxDistance = 0.01) {
return Math.abs(value - target) <= maxDistance;
}
function calcAxisDelta(delta, source, target, origin = 0.5) {
delta.origin = origin;
delta.originPoint = indexLegacy.mix(source.min, source.max, delta.origin);
delta.scale = calcLength(target) / calcLength(source);
if (isNear(delta.scale, 1, 0.0001) || isNaN(delta.scale))
delta.scale = 1;
delta.translate =
indexLegacy.mix(target.min, target.max, delta.origin) - delta.originPoint;
if (isNear(delta.translate) || isNaN(delta.translate))
delta.translate = 0;
}
function calcBoxDelta(delta, source, target, origin) {
calcAxisDelta(delta.x, source.x, target.x, origin ? origin.originX : undefined);
calcAxisDelta(delta.y, source.y, target.y, origin ? origin.originY : undefined);
}
function calcRelativeAxis(target, relative, parent) {
target.min = parent.min + relative.min;
target.max = target.min + calcLength(relative);
}
function calcRelativeBox(target, relative, parent) {
calcRelativeAxis(target.x, relative.x, parent.x);
calcRelativeAxis(target.y, relative.y, parent.y);
}
function calcRelativeAxisPosition(target, layout, parent) {
target.min = layout.min - parent.min;
target.max = target.min + calcLength(layout);
}
function calcRelativePosition(target, layout, parent) {
calcRelativeAxisPosition(target.x, layout.x, parent.x);
calcRelativeAxisPosition(target.y, layout.y, parent.y);
}
/**
* Apply constraints to a point. These constraints are both physical along an
* axis, and an elastic factor that determines how much to constrain the point
* by if it does lie outside the defined parameters.
*/
function applyConstraints(point, { min, max }, elastic) {
if (min !== undefined && point < min) {
// If we have a min point defined, and this is outside of that, constrain
point = elastic ? indexLegacy.mix(min, point, elastic.min) : Math.max(point, min);
}
else if (max !== undefined && point > max) {
// If we have a max point defined, and this is outside of that, constrain
point = elastic ? indexLegacy.mix(max, point, elastic.max) : Math.min(point, max);
}
return point;
}
/**
* Calculate constraints in terms of the viewport when defined relatively to the
* measured axis. This is measured from the nearest edge, so a max constraint of 200
* on an axis with a max value of 300 would return a constraint of 500 - axis length
*/
function calcRelativeAxisConstraints(axis, min, max) {
return {
min: min !== undefined ? axis.min + min : undefined,
max: max !== undefined
? axis.max + max - (axis.max - axis.min)
: undefined,
};
}
/**
* Calculate constraints in terms of the viewport when
* defined relatively to the measured bounding box.
*/
function calcRelativeConstraints(layoutBox, { top, left, bottom, right }) {
return {
x: calcRelativeAxisConstraints(layoutBox.x, left, right),
y: calcRelativeAxisConstraints(layoutBox.y, top, bottom),
};
}
/**
* Calculate viewport constraints when defined as another viewport-relative axis
*/
function calcViewportAxisConstraints(layoutAxis, constraintsAxis) {
let min = constraintsAxis.min - layoutAxis.min;
let max = constraintsAxis.max - layoutAxis.max;
// If the constraints axis is actually smaller than the layout axis then we can
// flip the constraints
if (constraintsAxis.max - constraintsAxis.min <
layoutAxis.max - layoutAxis.min) {
[min, max] = [max, min];
}
return { min, max };
}
/**
* Calculate viewport constraints when defined as another viewport-relative box
*/
function calcViewportConstraints(layoutBox, constraintsBox) {
return {
x: calcViewportAxisConstraints(layoutBox.x, constraintsBox.x),
y: calcViewportAxisConstraints(layoutBox.y, constraintsBox.y),
};
}
/**
* Calculate a transform origin relative to the source axis, between 0-1, that results
* in an asthetically pleasing scale/transform needed to project from source to target.
*/
function calcOrigin(source, target) {
let origin = 0.5;
const sourceLength = calcLength(source);
const targetLength = calcLength(target);
if (targetLength > sourceLength) {
origin = indexLegacy.progress(target.min, target.max - sourceLength, source.min);
}
else if (sourceLength > targetLength) {
origin = indexLegacy.progress(source.min, source.max - targetLength, target.min);
}
return indexLegacy.clamp(0, 1, origin);
}
/**
* Rebase the calculated viewport constraints relative to the layout.min point.
*/
function rebaseAxisConstraints(layout, constraints) {
const relativeConstraints = {};
if (constraints.min !== undefined) {
relativeConstraints.min = constraints.min - layout.min;
}
if (constraints.max !== undefined) {
relativeConstraints.max = constraints.max - layout.min;
}
return relativeConstraints;
}
const defaultElastic = 0.35;
/**
* Accepts a dragElastic prop and returns resolved elastic values for each axis.
*/
function resolveDragElastic(dragElastic = defaultElastic) {
if (dragElastic === false) {
dragElastic = 0;
}
else if (dragElastic === true) {
dragElastic = defaultElastic;
}
return {
x: resolveAxisElastic(dragElastic, "left", "right"),
y: resolveAxisElastic(dragElastic, "top", "bottom"),
};
}
function resolveAxisElastic(dragElastic, minLabel, maxLabel) {
return {
min: resolvePointElastic(dragElastic, minLabel),
max: resolvePointElastic(dragElastic, maxLabel),
};
}
function resolvePointElastic(dragElastic, label) {
return typeof dragElastic === "number"
? dragElastic
: dragElastic[label] || 0;
}
function eachAxis(callback) {
return [callback("x"), callback("y")];
}
// Fixes https://github.com/framer/motion/issues/2270
const getContextWindow = ({ current }) => {
return current ? current.ownerDocument.defaultView : null;
};
const elementDragControls = new WeakMap();
/**
*
*/
// let latestPointerEvent: PointerEvent
class VisualElementDragControls {
constructor(visualElement) {
// This is a reference to the global drag gesture lock, ensuring only one component
// can "capture" the drag of one or both axes.
// TODO: Look into moving this into pansession?
this.openGlobalLock = null;
this.isDragging = false;
this.currentDirection = null;
this.originPoint = { x: 0, y: 0 };
/**
* The permitted boundaries of travel, in pixels.
*/
this.constraints = false;
this.hasMutatedConstraints = false;
/**
* The per-axis resolved elastic values.
*/
this.elastic = indexLegacy.createBox();
this.visualElement = visualElement;
}
start(originEvent, { snapToCursor = false } = {}) {
/**
* Don't start dragging if this component is exiting
*/
const { presenceContext } = this.visualElement;
if (presenceContext && presenceContext.isPresent === false)
return;
const onSessionStart = (event) => {
const { dragSnapToOrigin } = this.getProps();
// Stop or pause any animations on both axis values immediately. This allows the user to throw and catch
// the component.
dragSnapToOrigin ? this.pauseAnimation() : this.stopAnimation();
if (snapToCursor) {
this.snapToCursor(extractEventInfo(event, "page").point);
}
};
const onStart = (event, info) => {
// Attempt to grab the global drag gesture lock - maybe make this part of PanSession
const { drag, dragPropagation, onDragStart } = this.getProps();
if (drag && !dragPropagation) {
if (this.openGlobalLock)
this.openGlobalLock();
this.openGlobalLock = getGlobalLock(drag);
// If we don 't have the lock, don't start dragging
if (!this.openGlobalLock)
return;
}
this.isDragging = true;
this.currentDirection = null;
this.resolveConstraints();
if (this.visualElement.projection) {
this.visualElement.projection.isAnimationBlocked = true;
this.visualElement.projection.target = undefined;
}
/**
* Record gesture origin
*/
eachAxis((axis) => {
let current = this.getAxisMotionValue(axis).get() || 0;
/**
* If the MotionValue is a percentage value convert to px
*/
if (indexLegacy.percent.test(current)) {
const { projection } = this.visualElement;
if (projection && projection.layout) {
const measuredAxis = projection.layout.layoutBox[axis];
if (measuredAxis) {
const length = calcLength(measuredAxis);
current = length * (parseFloat(current) / 100);
}
}
}
this.originPoint[axis] = current;
});
// Fire onDragStart event
if (onDragStart) {
indexLegacy.frame.update(() => onDragStart(event, info), false, true);
}
const { animationState } = this.visualElement;
animationState && animationState.setActive("whileDrag", true);
};
const onMove = (event, info) => {
// latestPointerEvent = event
const { dragPropagation, dragDirectionLock, onDirectionLock, onDrag, } = this.getProps();
// If we didn't successfully receive the gesture lock, early return.
if (!dragPropagation && !this.openGlobalLock)
return;
const { offset } = info;
// Attempt to detect drag direction if directionLock is true
if (dragDirectionLock && this.currentDirection === null) {
this.currentDirection = getCurrentDirection(offset);
// If we've successfully set a direction, notify listener
if (this.currentDirection !== null) {
onDirectionLock && onDirectionLock(this.currentDirection);
}
return;
}
// Update each point with the latest position
this.updateAxis("x", info.point, offset);
this.updateAxis("y", info.point, offset);
/**
* Ideally we would leave the renderer to fire naturally at the end of
* this frame but if the element is about to change layout as the result
* of a re-render we want to ensure the browser can read the latest
* bounding box to ensure the pointer and element don't fall out of sync.
*/
this.visualElement.render();
/**
* This must fire after the render call as it might trigger a state
* change which itself might trigger a layout update.
*/
onDrag && onDrag(event, info);
};
const onSessionEnd = (event, info) => this.stop(event, info);
const resumeAnimation = () => eachAxis((axis) => {
var _a;
return this.getAnimationState(axis) === "paused" &&
((_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.play());
});
const { dragSnapToOrigin } = this.getProps();
this.panSession = new PanSession(originEvent, {
onSessionStart,
onStart,
onMove,
onSessionEnd,
resumeAnimation,
}, {
transformPagePoint: this.visualElement.getTransformPagePoint(),
dragSnapToOrigin,
contextWindow: getContextWindow(this.visualElement),
});
}
stop(event, info) {
const isDragging = this.isDragging;
this.cancel();
if (!isDragging)
return;
const { velocity } = info;
this.startAnimation(velocity);
const { onDragEnd } = this.getProps();
if (onDragEnd) {
indexLegacy.frame.update(() => onDragEnd(event, info));
}
}
cancel() {
this.isDragging = false;
const { projection, animationState } = this.visualElement;
if (projection) {
projection.isAnimationBlocked = false;
}
this.panSession && this.panSession.end();
this.panSession = undefined;
const { dragPropagation } = this.getProps();
if (!dragPropagation && this.openGlobalLock) {
this.openGlobalLock();
this.openGlobalLock = null;
}
animationState && animationState.setActive("whileDrag", false);
}
updateAxis(axis, _point, offset) {
const { drag } = this.getProps();
// If we're not dragging this axis, do an early return.
if (!offset || !shouldDrag(axis, drag, this.currentDirection))
return;
const axisValue = this.getAxisMotionValue(axis);
let next = this.originPoint[axis] + offset[axis];
// Apply constraints
if (this.constraints && this.constraints[axis]) {
next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
}
axisValue.set(next);
}
resolveConstraints() {
var _a;
const { dragConstraints, dragElastic } = this.getProps();
const layout = this.visualElement.projection &&
!this.visualElement.projection.layout
? this.visualElement.projection.measure(false)
: (_a = this.visualElement.projection) === null || _a === void 0 ? void 0 : _a.layout;
const prevConstraints = this.constraints;
if (dragConstraints && indexLegacy.isRefObject(dragConstraints)) {
if (!this.constraints) {
this.constraints = this.resolveRefConstraints();
}
}
else {
if (dragConstraints && layout) {
this.constraints = calcRelativeConstraints(layout.layoutBox, dragConstraints);
}
else {
this.constraints = false;
}
}
this.elastic = resolveDragElastic(dragElastic);
/**
* If we're outputting to external MotionValues, we want to rebase the measured constraints
* from viewport-relative to component-relative.
*/
if (prevConstraints !== this.constraints &&
layout &&
this.constraints &&
!this.hasMutatedConstraints) {
eachAxis((axis) => {
if (this.getAxisMotionValue(axis)) {
this.constraints[axis] = rebaseAxisConstraints(layout.layoutBox[axis], this.constraints[axis]);
}
});
}
}
resolveRefConstraints() {
const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
if (!constraints || !indexLegacy.isRefObject(constraints))
return false;
const constraintsElement = constraints.current;
indexLegacy.invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.");
const { projection } = this.visualElement;
// TODO
if (!projection || !projection.layout)
return false;
const constraintsBox = indexLegacy.measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
/**
* If there's an onMeasureDragConstraints listener we call it and
* if different constraints are returned, set constraints to that
*/
if (onMeasureDragConstraints) {
const userConstraints = onMeasureDragConstraints(indexLegacy.convertBoxToBoundingBox(measuredConstraints));
this.hasMutatedConstraints = !!userConstraints;
if (userConstraints) {
measuredConstraints = indexLegacy.convertBoundingBoxToBox(userConstraints);
}
}
return measuredConstraints;
}
startAnimation(velocity) {
const { drag, dragMomentum, dragElastic, dragTransition, dragSnapToOrigin, onDragTransitionEnd, } = this.getProps();
const constraints = this.constraints || {};
const momentumAnimations = eachAxis((axis) => {
if (!shouldDrag(axis, drag, this.currentDirection)) {
return;
}
let transition = (constraints && constraints[axis]) || {};
if (dragSnapToOrigin)
transition = { min: 0, max: 0 };
/**
* Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
* of spring animations so we should look into adding a disable spring option to `inertia`.
* We could do something here where we affect the `bounceStiffness` and `bounceDamping`
* using the value of `dragElastic`.
*/
const bounceStiffness = dragElastic ? 200 : 1000000;
const bounceDamping = dragElastic ? 40 : 10000000;
const inertia = {
type: "inertia",
velocity: dragMomentum ? velocity[axis] : 0,
bounceStiffness,
bounceDamping,
timeConstant: 750,
restDelta: 1,
restSpeed: 10,
...dragTransition,
...transition,
};
// If we're not animating on an externally-provided `MotionValue` we can use the
// component's animation controls which will handle interactions with whileHover (etc),
// otherwise we just have to animate the `MotionValue` itself.
return this.startAxisValueAnimation(axis, inertia);
});
// Run all animations and then resolve the new drag constraints.
return Promise.all(momentumAnimations).then(onDragTransitionEnd);
}
startAxisValueAnimation(axis, transition) {
const axisValue = this.getAxisMotionValue(axis);
return axisValue.start(indexLegacy.animateMotionValue(axis, axisValue, 0, transition));
}
stopAnimation() {
eachAxis((axis) => this.getAxisMotionValue(axis).stop());
}
pauseAnimation() {
eachAxis((axis) => { var _a; return (_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.pause(); });
}
getAnimationState(axis) {
var _a;
return (_a = this.getAxisMotionValue(axis).animation) === null || _a === void 0 ? void 0 : _a.state;
}
/**
* Drag works differently depending on which props are provided.
*
* - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
* - Otherwise, we apply the delta to the x/y motion values.
*/
getAxisMotionValue(axis) {
const dragKey = "_drag" + axis.toUpperCase();
const props = this.visualElement.getProps();
const externalMotionValue = props[dragKey];
return externalMotionValue
? externalMotionValue
: this.visualElement.getValue(axis, (props.initial ? props.initial[axis] : undefined) || 0);
}
snapToCursor(point) {
eachAxis((axis) => {
const { drag } = this.getProps();
// If we're not dragging this axis, do an early return.
if (!shouldDrag(axis, drag, this.currentDirection))
return;
const { projection } = this.visualElement;
const axisValue = this.getAxisMotionValue(axis);
if (projection && projection.layout) {
const { min, max } = projection.layout.layoutBox[axis];
axisValue.set(point[axis] - indexLegacy.mix(min, max, 0.5));
}
});
}
/**
* When the viewport resizes we want to check if the measured constraints
* have changed and, if so, reposition the element within those new constraints
* relative to where it was before the resize.
*/
scalePositionWithinConstraints() {
if (!this.visualElement.current)
return;
const { drag, dragConstraints } = this.getProps();
const { projection } = this.visualElement;
if (!indexLegacy.isRefObject(dragConstraints) || !projection || !this.constraints)
return;
/**
* Stop current animations as there can be visual glitching if we try to do
* this mid-animation
*/
this.stopAnimation();
/**
* Record the relative position of the dragged element relative to the
* constraints box and save as a progress value.
*/
const boxProgress = { x: 0, y: 0 };
eachAxis((axis) => {
const axisValue = this.getAxisMotionValue(axis);
if (axisValue) {
const latest = axisValue.get();
boxProgress[axis] = calcOrigin({ min: latest, max: latest }, this.constraints[axis]);
}
});
/**
* Update the layout of this element and resolve the latest drag constraints
*/
const { transformTemplate } = this.visualElement.getProps();
this.visualElement.current.style.transform = transformTemplate
? transformTemplate({}, "")
: "none";
projection.root && projection.root.updateScroll();
projection.updateLayout();
this.resolveConstraints();
/**
* For each axis, calculate the current progress of the layout axis
* within the new constraints.
*/
eachAxis((axis) => {
if (!shouldDrag(axis, drag, null))
return;
/**
* Calculate a new transform based on the previous box progress
*/
const axisValue = this.getAxisMotionValue(axis);
const { min, max } = this.constraints[axis];
axisValue.set(indexLegacy.mix(min, max, boxProgress[axis]));
});
}
addListeners() {
if (!this.visualElement.current)
return;
elementDragControls.set(this.visualElement, this);
const element = this.visualElement.current;
/**
* Attach a pointerdown event listener on this DOM element to initiate drag tracking.
*/
const stopPointerListener = addPointerEvent(element, "pointerdown", (event) => {
const { drag, dragListener = true } = this.getProps();
drag && dragListener && this.start(event);
});
const measureDragConstraints = () => {
const { dragConstraints } = this.getProps();
if (indexLegacy.isRefObject(dragConstraints)) {
this.constraints = this.resolveRefConstraints();
}
};
const { projection } = this.visualElement;
const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
if (projection && !projection.layout) {
projection.root && projection.root.updateScroll();
projection.updateLayout();
}
measureDragConstraints();
/**
* Attach a window resize listener to scale the draggable target within its defined
* constraints as the window resizes.
*/
const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
/**
* If the element's layout changes, calculate the delta and apply that to
* the drag gesture's origin point.
*/
const stopLayoutUpdateListener = projection.addEventListener("didUpdate", (({ delta, hasLayoutChanged }) => {
if (this.isDragging && hasLayoutChanged) {
eachAxis((axis) => {
const motionValue = this.getAxisMotionValue(axis);
if (!motionValue)
return;
this.originPoint[axis] += delta[axis].translate;
motionValue.set(motionValue.get() + delta[axis].translate);
});
this.visualElement.render();
}
}));
return () => {
stopResizeListener();
stopPointerListener();
stopMeasureLayoutListener();
stopLayoutUpdateListener && stopLayoutUpdateListener();
};
}
getProps() {
const props = this.visualElement.getProps();
const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true, } = props;
return {
...props,
drag,
dragDirectionLock,
dragPropagation,
dragConstraints,
dragElastic,
dragMomentum,
};
}
}
function shouldDrag(direction, drag, currentDirection) {
return ((drag === true || drag === direction) &&
(currentDirection === null || currentDirection === direction));
}
/**
* Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
* than the provided threshold, return `null`.
*
* @param offset - The x/y offset from origin.
* @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
*/
function getCurrentDirection(offset, lockThreshold = 10) {
let direction = null;
if (Math.abs(offset.y) > lockThreshold) {
direction = "y";
}
else if (Math.abs(offset.x) > lockThreshold) {
direction = "x";
}
return direction;
}
class DragGesture extends Feature {
constructor(node) {
super(node);
this.removeGroupControls = indexLegacy.noop;
this.removeListeners = indexLegacy.noop;
this.controls = new VisualElementDragControls(node);
}
mount() {
// If we've been provided a DragControls for manual control over the drag gesture,
// subscribe this component to it on mount.
const { dragControls } = this.node.getProps();
if (dragControls) {
this.removeGroupControls = dragControls.subscribe(this.controls);
}
this.removeListeners = this.controls.addListeners() || indexLegacy.noop;
}
unmount() {
this.removeGroupControls();
this.removeListeners();
}
}
const asyncHandler = (handler) => (event, info) => {
if (handler) {
indexLegacy.frame.update(() => handler(event, info));
}
};
class PanGesture extends Feature {
constructor() {
super(...arguments);
this.removePointerDownListener = indexLegacy.noop;
}
onPointerDown(pointerDownEvent) {
this.session = new PanSession(pointerDownEvent, this.createPanHandlers(), {
transformPagePoint: this.node.getTransformPagePoint(),
contextWindow: getContextWindow(this.node),
});
}
createPanHandlers() {
const { onPanSessionStart, onPanStart, onPan, onPanEnd } = this.node.getProps();
return {
onSessionStart: asyncHandler(onPanSessionStart),
onStart: asyncHandler(onPanStart),
onMove: onPan,
onEnd: (event, info) => {
delete this.session;
if (onPanEnd) {
indexLegacy.frame.update(() => onPanEnd(event, info));
}
},
};
}
mount() {
this.removePointerDownListener = addPointerEvent(this.node.current, "pointerdown", (event) => this.onPointerDown(event));
}
update() {
this.session && this.session.updateHandlers(this.createPanHandlers());
}
unmount() {
this.removePointerDownListener();
this.session && this.session.end();
}
}
const borders = ["TopLeft", "TopRight", "BottomLeft", "BottomRight"];
const numBorders = borders.length;
const asNumber = (value) => typeof value === "string" ? parseFloat(value) : value;
const isPx = (value) => typeof value === "number" || indexLegacy.px.test(value);
function mixValues(target, follow, lead, progress, shouldCrossfadeOpacity, isOnlyMember) {
if (shouldCrossfadeOpacity) {
target.opacity = indexLegacy.mix(0,
// TODO Reinstate this if only child
lead.opacity !== undefined ? lead.opacity : 1, easeCrossfadeIn(progress));
target.opacityExit = indexLegacy.mix(follow.opacity !== undefined ? follow.opacity : 1, 0, easeCrossfadeOut(progress));
}
else if (isOnlyMember) {
target.opacity = indexLegacy.mix(follow.opacity !== undefined ? follow.opacity : 1, lead.opacity !== undefined ? lead.opacity : 1, progress);
}
/**
* Mix border radius
*/
for (let i = 0; i < numBorders; i++) {
const borderLabel = `border${borders[i]}Radius`;
let followRadius = getRadius(follow, borderLabel);
let leadRadius = getRadius(lead, borderLabel);
if (followRadius === undefined && leadRadius === undefined)
continue;
followRadius || (followRadius = 0);
leadRadius || (leadRadius = 0);
const canMix = followRadius === 0 ||
leadRadius === 0 ||
isPx(followRadius) === isPx(leadRadius);
if (canMix) {
target[borderLabel] = Math.max(indexLegacy.mix(asNumber(followRadius), asNumber(leadRadius), progress), 0);
if (indexLegacy.percent.test(leadRadius) || indexLegacy.percent.test(followRadius)) {
target[borderLabel] += "%";
}
}
else {
target[borderLabel] = leadRadius;
}
}
/**
* Mix rotation
*/
if (follow.rotate || lead.rotate) {
target.rotate = indexLegacy.mix(follow.rotate || 0, lead.rotate || 0, progress);
}
}
function getRadius(values, radiusName) {
return values[radiusName] !== undefined
? values[radiusName]
: values.borderRadius;
}
// /**
// * We only want to mix the background color if there's a follow element
// * that we're not crossfading opacity between. For instance with switch
// * AnimateSharedLayout animations, this helps the illusion of a continuous
// * element being animated but also cuts down on the number of paints triggered
// * for elements where opacity is doing that work for us.
// */
// if (
// !hasFollowElement &&
// latestLeadValues.backgroundColor &&
// latestFollowValues.backgroundColor
// ) {
// /**
// * This isn't ideal performance-wise as mixColor is creating a new function every frame.
// * We could probably create a mixer that runs at the start of the animation but
// * the idea behind the crossfader is that it runs dynamically between two potentially
// * changing targets (ie opacity or borderRadius may be animating independently via variants)
// */
// leadState.backgroundColor = followState.backgroundColor = mixColor(
// latestFollowValues.backgroundColor as string,
// latestLeadValues.backgroundColor as string
// )(p)
// }
const easeCrossfadeIn = compress(0, 0.5, indexLegacy.circOut);
const easeCrossfadeOut = compress(0.5, 0.95, indexLegacy.noop);
function compress(min, max, easing) {
return (p) => {
// Could replace ifs with clamp
if (p < min)
return 0;
if (p > max)
return 1;
return easing(indexLegacy.progress(min, max, p));
};
}
/**
* Reset an axis to the provided origin box.
*
* This is a mutative operation.
*/
function copyAxisInto(axis, originAxis) {
axis.min = originAxis.min;
axis.max = originAxis.max;
}
/**
* Reset a box to the provided origin box.
*
* This is a mutative operation.
*/
function copyBoxInto(box, originBox) {
copyAxisInto(box.x, originBox.x);
copyAxisInto(box.y, originBox.y);
}
/**
* Remove a delta from a point. This is essentially the steps of applyPointDelta in reverse
*/
function removePointDelta(point, translate, scale, originPoint, boxScale) {
point -= translate;
point = indexLegacy.scalePoint(point, 1 / scale, originPoint);
if (boxScale !== undefined) {
point = indexLegacy.scalePoint(point, 1 / boxScale, originPoint);
}
return point;
}
/**
* Remove a delta from an axis. This is essentially the steps of applyAxisDelta in reverse
*/
function removeAxisDelta(axis, translate = 0, scale = 1, origin = 0.5, boxScale, originAxis = axis, sourceAxis = axis) {
if (indexLegacy.percent.test(translate)) {
translate = parseFloat(translate);
const relativeProgress = indexLegacy.mix(sourceAxis.min, sourceAxis.max, translate / 100);
translate = relativeProgress - sourceAxis.min;
}
if (typeof translate !== "number")
return;
let originPoint = indexLegacy.mix(originAxis.min, originAxis.max, origin);
if (axis === originAxis)
originPoint -= translate;
axis.min = removePointDelta(axis.min, translate, scale, originPoint, boxScale);
axis.max = removePointDelta(axis.max, translate, scale, originPoint, boxScale);
}
/**
* Remove a transforms from an axis. This is essentially the steps of applyAxisTransforms in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeAxisTransforms(axis, transforms, [key, scaleKey, originKey], origin, sourceAxis) {
removeAxisDelta(axis, transforms[key], transforms[scaleKey], transforms[originKey], transforms.scale, origin, sourceAxis);
}
/**
* The names of the motion values we want to apply as translation, scale and origin.
*/
const xKeys = ["x", "scaleX", "originX"];
const yKeys = ["y", "scaleY", "originY"];
/**
* Remove a transforms from an box. This is essentially the steps of applyAxisBox in reverse
* and acts as a bridge between motion values and removeAxisDelta
*/
function removeBoxTransforms(box, transforms, originBox, sourceBox) {
removeAxisTransforms(box.x, transforms, xKeys, originBox ? originBox.x : undefined, sourceBox ? sourceBox.x : undefined);
removeAxisTransforms(box.y, transforms, yKeys, originBox ? originBox.y : undefined, sourceBox ? sourceBox.y : undefined);
}
function isAxisDeltaZero(delta) {
return delta.translate === 0 && delta.scale === 1;
}
function isDeltaZero(delta) {
return isAxisDeltaZero(delta.x) && isAxisDeltaZero(delta.y);
}
function boxEquals(a, b) {
return (a.x.min === b.x.min &&
a.x.max === b.x.max &&
a.y.min === b.y.min &&
a.y.max === b.y.max);
}
function boxEqualsRounded(a, b) {
return (Math.round(a.x.min) === Math.round(b.x.min) &&
Math.round(a.x.max) === Math.round(b.x.max) &&
Math.round(a.y.min) === Math.round(b.y.min) &&
Math.round(a.y.max) === Math.round(b.y.max));
}
function aspectRatio(box) {
return calcLength(box.x) / calcLength(box.y);
}
class NodeStack {
constructor() {
this.members = [];
}
add(node) {
indexLegacy.addUniqueItem(this.members, node);
node.scheduleRender();
}
remove(node) {
indexLegacy.removeItem(this.members, node);
if (node === this.prevLead) {
this.prevLead = undefined;
}
if (node === this.lead) {
const prevLead = this.members[this.members.length - 1];
if (prevLead) {
this.promote(prevLead);
}
}
}
relegate(node) {
const indexOfNode = this.members.findIndex((member) => node === member);
if (indexOfNode === 0)
return false;
/**
* Find the next projection node that is present
*/
let prevLead;
for (let i = indexOfNode; i >= 0; i--) {
const member = this.members[i];
if (member.isPresent !== false) {
prevLead = member;
break;
}
}
if (prevLead) {
this.promote(prevLead);
return true;
}
else {
return false;
}
}
promote(node, preserveFollowOpacity) {
const prevLead = this.lead;
if (node === prevLead)
return;
this.prevLead = prevLead;
this.lead = node;
node.show();
if (prevLead) {
prevLead.instance && prevLead.scheduleRender();
node.scheduleRender();
node.resumeFrom = prevLead;
if (preserveFollowOpacity) {
node.resumeFrom.preserveOpacity = true;
}
if (prevLead.snapshot) {
node.snapshot = prevLead.snapshot;
node.snapshot.latestValues =
prevLead.animationValues || prevLead.latestValues;
}
if (node.root && node.root.isUpdating) {
node.isLayoutDirty = true;
}
const { crossfade } = node.options;
if (crossfade === false) {
prevLead.hide();
}
/**
* TODO:
* - Test border radius when previous node was deleted
* - boxShadow mixing
* - Shared between element A in scrolled container and element B (scroll stays the same or changes)
* - Shared between element A in transformed container and element B (transform stays the same or changes)
* - Shared between element A in scrolled page and element B (scroll stays the same or changes)
* ---
* - Crossfade opacity of root nodes
* - layoutId changes after animation
* - layoutId changes mid animation
*/
}
}
exitAnimationComplete() {
this.members.forEach((node) => {
const { options, resumingFrom } = node;
options.onExitComplete && options.onExitComplete();
if (resumingFrom) {
resumingFrom.options.onExitComplete &&
resumingFrom.options.onExitComplete();
}
});
}
scheduleRender() {
this.members.forEach((node) => {
node.instance && node.scheduleRender(false);
});
}
/**
* Clear any leads that have been removed this render to prevent them from being
* used in future animations and to prevent memory leaks
*/
removeLeadSnapshot() {
if (this.lead && this.lead.snapshot) {
this.lead.snapshot = undefined;
}
}
}
function buildProjectionTransform(delta, treeScale, latestTransform) {
let transform = "";
/**
* The translations we use to calculate are always relative to the viewport coordinate space.
* But when we apply scales, we also scale the coordinate space of an element and its children.
* For instance if we have a treeScale (the culmination of all parent scales) of 0.5 and we need
* to move an element 100 pixels, we actually need to move it 200 in within that scaled space.
*/
const xTranslate = delta.x.translate / treeScale.x;
const yTranslate = delta.y.translate / treeScale.y;
if (xTranslate || yTranslate) {
transform = `translate3d(${xTranslate}px, ${yTranslate}px, 0) `;
}
/**
* Apply scale correction for the tree transform.
* This will apply scale to the screen-orientated axes.
*/
if (treeScale.x !== 1 || treeScale.y !== 1) {
transform += `scale(${1 / treeScale.x}, ${1 / treeScale.y}) `;
}
if (latestTransform) {
const { rotate, rotateX, rotateY } = latestTransform;
if (rotate)
transform += `rotate(${rotate}deg) `;
if (rotateX)
transform += `rotateX(${rotateX}deg) `;
if (rotateY)
transform += `rotateY(${rotateY}deg) `;
}
/**
* Apply scale to match the size of the element to the size we want it.
* This will apply scale to the element-orientated axes.
*/
const elementScaleX = delta.x.scale * treeScale.x;
const elementScaleY = delta.y.scale * treeScale.y;
if (elementScaleX !== 1 || elementScaleY !== 1) {
transform += `scale(${elementScaleX}, ${elementScaleY})`;
}
return transform || "none";
}
const compareByDepth = (a, b) => a.depth - b.depth;
class FlatTree {
constructor() {
this.children = [];
this.isDirty = false;
}
add(child) {
indexLegacy.addUniqueItem(this.children, child);
this.isDirty = true;
}
remove(child) {
indexLegacy.removeItem(this.children, child);
this.isDirty = true;
}
forEach(callback) {
this.isDirty && this.children.sort(compareByDepth);
this.isDirty = false;
this.children.forEach(callback);
}
}
/**
* This should only ever be modified on the client otherwise it'll
* persist through server requests. If we need instanced states we
* could lazy-init via root.
*/
const globalProjectionState = {
/**
* Global flag as to whether the tree has animated since the last time
* we resized the window
*/
hasAnimatedSinceResize: true,
/**
* We set this to true once, on the first update. Any nodes added to the tree beyond that
* update will be given a `data-projection-id` attribute.
*/
hasEverUpdated: false,
};
function record(data) {
if (window.MotionDebug) {
window.MotionDebug.record(data);
}
}
const transformAxes = ["", "X", "Y", "Z"];
const hiddenVisibility = { visibility: "hidden" };
/**
* We use 1000 as the animation target as 0-1000 maps better to pixels than 0-1
* which has a noticeable difference in spring animations
*/
const animationTarget = 1000;
let id$1 = 0;
/**
* Use a mutable data object for debug data so as to not create a new
* object every frame.
*/
const projectionFrameData = {
type: "projectionFrame",
totalNodes: 0,
resolvedTargetDeltas: 0,
recalculatedProjection: 0,
};
function createProjectionNode({ attachResizeListener, defaultParent, measureScroll, checkIsScrollRoot, resetTransform, }) {
return class ProjectionNode {
constructor(latestValues = {}, parent = defaultParent === null || defaultParent === void 0 ? void 0 : defaultParent()) {
/**
* A unique ID generated for every projection node.
*/
this.id = id$1++;
/**
* An id that represents a unique session instigated by startUpdate.
*/
this.animationId = 0;
/**
* A Set containing all this component's children. This is used to iterate
* through the children.
*
* TODO: This could be faster to iterate as a flat array stored on the root node.
*/
this.children = new Set();
/**
* Options for the node. We use this to configure what kind of layout animations
* we should perform (if any).
*/
this.options = {};
/**
* We use this to detect when its safe to shut down part of a projection tree.
* We have to keep projecting children for scale correction and relative projection
* until all their parents stop performing layout animations.
*/
this.isTreeAnimating = false;
this.isAnimationBlocked = false;
/**
* Flag to true if we think this layout has been changed. We can't always know this,
* currently we set it to true every time a component renders, or if it has a layoutDependency
* if that has changed between renders. Additionally, components can be grouped by LayoutGroup
* and if one node is dirtied, they all are.
*/
this.isLayoutDirty = false;
/**
* Flag to true if we think the projection calculations for this node needs
* recalculating as a result of an updated transform or layout animation.
*/
this.isProjectionDirty = false;
/**
* Flag to true if the layout *or* transform has changed. This then gets propagated
* throughout the projection tree, forcing any element below to recalculate on the next frame.
*/
this.isSharedProjectionDirty = false;
/**
* Flag transform dirty. This gets propagated throughout the whole tree but is only
* respected by shared nodes.
*/
this.isTransformDirty = false;
/**
* Block layout updates for instant layout transitions throughout the tree.
*/
this.updateManuallyBlocked = false;
this.updateBlockedByResize = false;
/**
* Set to true between the start of the first `willUpdate` call and the end of the `didUpdate`
* call.
*/
this.isUpdating = false;
/**
* If this is an SVG element we currently disable projection transforms
*/
this.isSVG = false;
/**
* Flag to true (during promotion) if a node doing an instant layout transition needs to reset
* its projection styles.
*/
this.needsReset = false;
/**
* Flags whether this node should have its transform reset prior to measuring.
*/
this.shouldResetTransform = false;
/**
* An object representing the calculated contextual/accumulated/tree scale.
* This will be used to scale calculcated projection transforms, as these are
* calculated in screen-space but need to be scaled for elements to layoutly
* make it to their calculated destinations.
*
* TODO: Lazy-init
*/
this.treeScale = { x: 1, y: 1 };
/**
*
*/
this.eventHandlers = new Map();
this.hasTreeAnimated = false;
// Note: Currently only running on root node
this.updateScheduled = false;
this.projectionUpdateScheduled = false;
this.checkUpdateFailed = () => {
if (this.isUpdating) {
this.isUpdating = false;
this.clearAllSnapshots();
}
};
/**
* This is a multi-step process as shared nodes might be of different depths. Nodes
* are sorted by depth order, so we need to resolve the entire tree before moving to
* the next step.
*/
this.updateProjection = () => {
this.projectionUpdateScheduled = false;
/**
* Reset debug counts. Manually resetting rather than creating a new
* object each frame.
*/
projectionFrameData.totalNodes =
projectionFrameData.resolvedTargetDeltas =
projectionFrameData.recalculatedProjection =
0;
this.nodes.forEach(propagateDirtyNodes);
this.nodes.forEach(resolveTargetDelta);
this.nodes.forEach(calcProjection);
this.nodes.forEach(cleanDirtyNodes);
record(projectionFrameData);
};
this.hasProjected = false;
this.isVisible = true;
this.animationProgress = 0;
/**
* Shared layout
*/
// TODO Only running on root node
this.sharedNodes = new Map();
this.latestValues = latestValues;
this.root = parent ? parent.root || parent : this;
this.path = parent ? [...parent.path, parent] : [];
this.parent = parent;
this.depth = parent ? parent.depth + 1 : 0;
for (let i = 0; i < this.path.length; i++) {
this.path[i].shouldResetTransform = true;
}
if (this.root === this)
this.nodes = new FlatTree();
}
addEventListener(name, handler) {
if (!this.eventHandlers.has(name)) {
this.eventHandlers.set(name, new indexLegacy.SubscriptionManager());
}
return this.eventHandlers.get(name).add(handler);
}
notifyListeners(name, ...args) {
const subscriptionManager = this.eventHandlers.get(name);
subscriptionManager && subscriptionManager.notify(...args);
}
hasListeners(name) {
return this.eventHandlers.has(name);
}
/**
* Lifecycles
*/
mount(instance, isLayoutDirty = this.root.hasTreeAnimated) {
if (this.instance)
return;
this.isSVG = indexLegacy.isSVGElement(instance);
this.instance = instance;
const { layoutId, layout, visualElement } = this.options;
if (visualElement && !visualElement.current) {
visualElement.mount(instance);
}
this.root.nodes.add(this);
this.parent && this.parent.children.add(this);
if (isLayoutDirty && (layout || layoutId)) {
this.isLayoutDirty = true;
}
if (attachResizeListener) {
let cancelDelay;
const resizeUnblockUpdate = () => (this.root.updateBlockedByResize = false);
attachResizeListener(instance, () => {
this.root.updateBlockedByResize = true;
cancelDelay && cancelDelay();
cancelDelay = indexLegacy.delay(resizeUnblockUpdate, 250);
if (globalProjectionState.hasAnimatedSinceResize) {
globalProjectionState.hasAnimatedSinceResize = false;
this.nodes.forEach(finishAnimation);
}
});
}
if (layoutId) {
this.root.registerSharedNode(layoutId, this);
}
// Only register the handler if it requires layout animation
if (this.options.animate !== false &&
visualElement &&
(layoutId || layout)) {
this.addEventListener("didUpdate", ({ delta, hasLayoutChanged, hasRelativeTargetChanged, layout: newLayout, }) => {
if (this.isTreeAnimationBlocked()) {
this.target = undefined;
this.relativeTarget = undefined;
return;
}
// TODO: Check here if an animation exists
const layoutTransition = this.options.transition ||
visualElement.getDefaultTransition() ||
defaultLayoutTransition;
const { onLayoutAnimationStart, onLayoutAnimationComplete, } = visualElement.getProps();
/**
* The target layout of the element might stay the same,
* but its position relative to its parent has changed.
*/
const targetChanged = !this.targetLayout ||
!boxEqualsRounded(this.targetLayout, newLayout) ||
hasRelativeTargetChanged;
/**
* If the layout hasn't seemed to have changed, it might be that the
* element is visually in the same place in the document but its position
* relative to its parent has indeed changed. So here we check for that.
*/
const hasOnlyRelativeTargetChanged = !hasLayoutChanged && hasRelativeTargetChanged;
if (this.options.layoutRoot ||
(this.resumeFrom && this.resumeFrom.instance) ||
hasOnlyRelativeTargetChanged ||
(hasLayoutChanged &&
(targetChanged || !this.currentAnimation))) {
if (this.resumeFrom) {
this.resumingFrom = this.resumeFrom;
this.resumingFrom.resumingFrom = undefined;
}
this.setAnimationOrigin(delta, hasOnlyRelativeTargetChanged);
const animationOptions = {
...indexLegacy.getValueTransition(layoutTransition, "layout"),
onPlay: onLayoutAnimationStart,
onComplete: onLayoutAnimationComplete,
};
if (visualElement.shouldReduceMotion ||
this.options.layoutRoot) {
animationOptions.delay = 0;
animationOptions.type = false;
}
this.startAnimation(animationOptions);
}
else {
/**
* If the layout hasn't changed and we have an animation that hasn't started yet,
* finish it immediately. Otherwise it will be animating from a location
* that was probably never commited to screen and look like a jumpy box.
*/
if (!hasLayoutChanged) {
finishAnimation(this);
}
if (this.isLead() && this.options.onExitComplete) {
this.options.onExitComplete();
}
}
this.targetLayout = newLayout;
});
}
}
unmount() {
this.options.layoutId && this.willUpdate();
this.root.nodes.remove(this);
const stack = this.getStack();
stack && stack.remove(this);
this.parent && this.parent.children.delete(this);
this.instance = undefined;
indexLegacy.cancelFrame(this.updateProjection);
}
// only on the root
blockUpdate() {
this.updateManuallyBlocked = true;
}
unblockUpdate() {
this.updateManuallyBlocked = false;
}
isUpdateBlocked() {
return this.updateManuallyBlocked || this.updateBlockedByResize;
}
isTreeAnimationBlocked() {
return (this.isAnimationBlocked ||
(this.parent && this.parent.isTreeAnimationBlocked()) ||
false);
}
// Note: currently only running on root node
startUpdate() {
if (this.isUpdateBlocked())
return;
this.isUpdating = true;
this.nodes && this.nodes.forEach(resetRotation);
this.animationId++;
}
getTransformTemplate() {
const { visualElement } = this.options;
return visualElement && visualElement.getProps().transformTemplate;
}
willUpdate(shouldNotifyListeners = true) {
this.root.hasTreeAnimated = true;
if (this.root.isUpdateBlocked()) {
this.options.onExitComplete && this.options.onExitComplete();
return;
}
!this.root.isUpdating && this.root.startUpdate();
if (this.isLayoutDirty)
return;
this.isLayoutDirty = true;
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
node.shouldResetTransform = true;
node.updateScroll("snapshot");
if (node.options.layoutRoot) {
node.willUpdate(false);
}
}
const { layoutId, layout } = this.options;
if (layoutId === undefined && !layout)
return;
const transformTemplate = this.getTransformTemplate();
this.prevTransformTemplateValue = transformTemplate
? transformTemplate(this.latestValues, "")
: undefined;
this.updateSnapshot();
shouldNotifyListeners && this.notifyListeners("willUpdate");
}
update() {
this.updateScheduled = false;
const updateWasBlocked = this.isUpdateBlocked();
// When doing an instant transition, we skip the layout update,
// but should still clean up the measurements so that the next
// snapshot could be taken correctly.
if (updateWasBlocked) {
this.unblockUpdate();
this.clearAllSnapshots();
this.nodes.forEach(clearMeasurements);
return;
}
if (!this.isUpdating) {
this.nodes.forEach(clearIsLayoutDirty);
}
this.isUpdating = false;
/**
* Write
*/
this.nodes.forEach(resetTransformStyle);
/**
* Read ==================
*/
// Update layout measurements of updated children
this.nodes.forEach(updateLayout);
/**
* Write
*/
// Notify listeners that the layout is updated
this.nodes.forEach(notifyLayoutUpdate);
this.clearAllSnapshots();
/**
* Manually flush any pending updates. Ideally
* we could leave this to the following requestAnimationFrame but this seems
* to leave a flash of incorrectly styled content.
*/
const now = performance.now();
indexLegacy.frameData.delta = indexLegacy.clamp(0, 1000 / 60, now - indexLegacy.frameData.timestamp);
indexLegacy.frameData.timestamp = now;
indexLegacy.frameData.isProcessing = true;
indexLegacy.steps.update.process(indexLegacy.frameData);
indexLegacy.steps.preRender.process(indexLegacy.frameData);
indexLegacy.steps.render.process(indexLegacy.frameData);
indexLegacy.frameData.isProcessing = false;
}
didUpdate() {
if (!this.updateScheduled) {
this.updateScheduled = true;
queueMicrotask(() => this.update());
}
}
clearAllSnapshots() {
this.nodes.forEach(clearSnapshot);
this.sharedNodes.forEach(removeLeadSnapshots);
}
scheduleUpdateProjection() {
if (!this.projectionUpdateScheduled) {
this.projectionUpdateScheduled = true;
indexLegacy.frame.preRender(this.updateProjection, false, true);
}
}
scheduleCheckAfterUnmount() {
/**
* If the unmounting node is in a layoutGroup and did trigger a willUpdate,
* we manually call didUpdate to give a chance to the siblings to animate.
* Otherwise, cleanup all snapshots to prevents future nodes from reusing them.
*/
indexLegacy.frame.postRender(() => {
if (this.isLayoutDirty) {
this.root.didUpdate();
}
else {
this.root.checkUpdateFailed();
}
});
}
/**
* Update measurements
*/
updateSnapshot() {
if (this.snapshot || !this.instance)
return;
this.snapshot = this.measure();
}
updateLayout() {
if (!this.instance)
return;
// TODO: Incorporate into a forwarded scroll offset
this.updateScroll();
if (!(this.options.alwaysMeasureLayout && this.isLead()) &&
!this.isLayoutDirty) {
return;
}
/**
* When a node is mounted, it simply resumes from the prevLead's
* snapshot instead of taking a new one, but the ancestors scroll
* might have updated while the prevLead is unmounted. We need to
* update the scroll again to make sure the layout we measure is
* up to date.
*/
if (this.resumeFrom && !this.resumeFrom.instance) {
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
node.updateScroll();
}
}
const prevLayout = this.layout;
this.layout = this.measure(false);
this.layoutCorrected = indexLegacy.createBox();
this.isLayoutDirty = false;
this.projectionDelta = undefined;
this.notifyListeners("measure", this.layout.layoutBox);
const { visualElement } = this.options;
visualElement &&
visualElement.notify("LayoutMeasure", this.layout.layoutBox, prevLayout ? prevLayout.layoutBox : undefined);
}
updateScroll(phase = "measure") {
let needsMeasurement = Boolean(this.options.layoutScroll && this.instance);
if (this.scroll &&
this.scroll.animationId === this.root.animationId &&
this.scroll.phase === phase) {
needsMeasurement = false;
}
if (needsMeasurement) {
this.scroll = {
animationId: this.root.animationId,
phase,
isRoot: checkIsScrollRoot(this.instance),
offset: measureScroll(this.instance),
};
}
}
resetTransform() {
if (!resetTransform)
return;
const isResetRequested = this.isLayoutDirty || this.shouldResetTransform;
const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta);
const transformTemplate = this.getTransformTemplate();
const transformTemplateValue = transformTemplate
? transformTemplate(this.latestValues, "")
: undefined;
const transformTemplateHasChanged = transformTemplateValue !== this.prevTransformTemplateValue;
if (isResetRequested &&
(hasProjection ||
indexLegacy.hasTransform(this.latestValues) ||
transformTemplateHasChanged)) {
resetTransform(this.instance, transformTemplateValue);
this.shouldResetTransform = false;
this.scheduleRender();
}
}
measure(removeTransform = true) {
const pageBox = this.measurePageBox();
let layoutBox = this.removeElementScroll(pageBox);
/**
* Measurements taken during the pre-render stage
* still have transforms applied so we remove them
* via calculation.
*/
if (removeTransform) {
layoutBox = this.removeTransform(layoutBox);
}
roundBox(layoutBox);
return {
animationId: this.root.animationId,
measuredBox: pageBox,
layoutBox,
latestValues: {},
source: this.id,
};
}
measurePageBox() {
const { visualElement } = this.options;
if (!visualElement)
return indexLegacy.createBox();
const box = visualElement.measureViewportBox();
// Remove viewport scroll to give page-relative coordinates
const { scroll } = this.root;
if (scroll) {
indexLegacy.translateAxis(box.x, scroll.offset.x);
indexLegacy.translateAxis(box.y, scroll.offset.y);
}
return box;
}
removeElementScroll(box) {
const boxWithoutScroll = indexLegacy.createBox();
copyBoxInto(boxWithoutScroll, box);
/**
* Performance TODO: Keep a cumulative scroll offset down the tree
* rather than loop back up the path.
*/
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
const { scroll, options } = node;
if (node !== this.root && scroll && options.layoutScroll) {
/**
* If this is a new scroll root, we want to remove all previous scrolls
* from the viewport box.
*/
if (scroll.isRoot) {
copyBoxInto(boxWithoutScroll, box);
const { scroll: rootScroll } = this.root;
/**
* Undo the application of page scroll that was originally added
* to the measured bounding box.
*/
if (rootScroll) {
indexLegacy.translateAxis(boxWithoutScroll.x, -rootScroll.offset.x);
indexLegacy.translateAxis(boxWithoutScroll.y, -rootScroll.offset.y);
}
}
indexLegacy.translateAxis(boxWithoutScroll.x, scroll.offset.x);
indexLegacy.translateAxis(boxWithoutScroll.y, scroll.offset.y);
}
}
return boxWithoutScroll;
}
applyTransform(box, transformOnly = false) {
const withTransforms = indexLegacy.createBox();
copyBoxInto(withTransforms, box);
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
if (!transformOnly &&
node.options.layoutScroll &&
node.scroll &&
node !== node.root) {
indexLegacy.transformBox(withTransforms, {
x: -node.scroll.offset.x,
y: -node.scroll.offset.y,
});
}
if (!indexLegacy.hasTransform(node.latestValues))
continue;
indexLegacy.transformBox(withTransforms, node.latestValues);
}
if (indexLegacy.hasTransform(this.latestValues)) {
indexLegacy.transformBox(withTransforms, this.latestValues);
}
return withTransforms;
}
removeTransform(box) {
const boxWithoutTransform = indexLegacy.createBox();
copyBoxInto(boxWithoutTransform, box);
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i];
if (!node.instance)
continue;
if (!indexLegacy.hasTransform(node.latestValues))
continue;
indexLegacy.hasScale(node.latestValues) && node.updateSnapshot();
const sourceBox = indexLegacy.createBox();
const nodeBox = node.measurePageBox();
copyBoxInto(sourceBox, nodeBox);
removeBoxTransforms(boxWithoutTransform, node.latestValues, node.snapshot ? node.snapshot.layoutBox : undefined, sourceBox);
}
if (indexLegacy.hasTransform(this.latestValues)) {
removeBoxTransforms(boxWithoutTransform, this.latestValues);
}
return boxWithoutTransform;
}
setTargetDelta(delta) {
this.targetDelta = delta;
this.root.scheduleUpdateProjection();
this.isProjectionDirty = true;
}
setOptions(options) {
this.options = {
...this.options,
...options,
crossfade: options.crossfade !== undefined ? options.crossfade : true,
};
}
clearMeasurements() {
this.scroll = undefined;
this.layout = undefined;
this.snapshot = undefined;
this.prevTransformTemplateValue = undefined;
this.targetDelta = undefined;
this.target = undefined;
this.isLayoutDirty = false;
}
forceRelativeParentToResolveTarget() {
if (!this.relativeParent)
return;
/**
* If the parent target isn't up-to-date, force it to update.
* This is an unfortunate de-optimisation as it means any updating relative
* projection will cause all the relative parents to recalculate back
* up the tree.
*/
if (this.relativeParent.resolvedRelativeTargetAt !==
indexLegacy.frameData.timestamp) {
this.relativeParent.resolveTargetDelta(true);
}
}
resolveTargetDelta(forceRecalculation = false) {
var _a;
/**
* Once the dirty status of nodes has been spread through the tree, we also
* need to check if we have a shared node of a different depth that has itself
* been dirtied.
*/
const lead = this.getLead();
this.isProjectionDirty || (this.isProjectionDirty = lead.isProjectionDirty);
this.isTransformDirty || (this.isTransformDirty = lead.isTransformDirty);
this.isSharedProjectionDirty || (this.isSharedProjectionDirty = lead.isSharedProjectionDirty);
const isShared = Boolean(this.resumingFrom) || this !== lead;
/**
* We don't use transform for this step of processing so we don't
* need to check whether any nodes have changed transform.
*/
const canSkip = !(forceRecalculation ||
(isShared && this.isSharedProjectionDirty) ||
this.isProjectionDirty ||
((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isProjectionDirty) ||
this.attemptToResolveRelativeTarget);
if (canSkip)
return;
const { layout, layoutId } = this.options;
/**
* If we have no layout, we can't perform projection, so early return
*/
if (!this.layout || !(layout || layoutId))
return;
this.resolvedRelativeTargetAt = indexLegacy.frameData.timestamp;
/**
* If we don't have a targetDelta but do have a layout, we can attempt to resolve
* a relativeParent. This will allow a component to perform scale correction
* even if no animation has started.
*/
// TODO If this is unsuccessful this currently happens every frame
if (!this.targetDelta && !this.relativeTarget) {
// TODO: This is a semi-repetition of further down this function, make DRY
const relativeParent = this.getClosestProjectingParent();
if (relativeParent &&
relativeParent.layout &&
this.animationProgress !== 1) {
this.relativeParent = relativeParent;
this.forceRelativeParentToResolveTarget();
this.relativeTarget = indexLegacy.createBox();
this.relativeTargetOrigin = indexLegacy.createBox();
calcRelativePosition(this.relativeTargetOrigin, this.layout.layoutBox, relativeParent.layout.layoutBox);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
}
else {
this.relativeParent = this.relativeTarget = undefined;
}
}
/**
* If we have no relative target or no target delta our target isn't valid
* for this frame.
*/
if (!this.relativeTarget && !this.targetDelta)
return;
/**
* Lazy-init target data structure
*/
if (!this.target) {
this.target = indexLegacy.createBox();
this.targetWithTransforms = indexLegacy.createBox();
}
/**
* If we've got a relative box for this component, resolve it into a target relative to the parent.
*/
if (this.relativeTarget &&
this.relativeTargetOrigin &&
this.relativeParent &&
this.relativeParent.target) {
this.forceRelativeParentToResolveTarget();
calcRelativeBox(this.target, this.relativeTarget, this.relativeParent.target);
/**
* If we've only got a targetDelta, resolve it into a target
*/
}
else if (this.targetDelta) {
if (Boolean(this.resumingFrom)) {
// TODO: This is creating a new object every frame
this.target = this.applyTransform(this.layout.layoutBox);
}
else {
copyBoxInto(this.target, this.layout.layoutBox);
}
indexLegacy.applyBoxDelta(this.target, this.targetDelta);
}
else {
/**
* If no target, use own layout as target
*/
copyBoxInto(this.target, this.layout.layoutBox);
}
/**
* If we've been told to attempt to resolve a relative target, do so.
*/
if (this.attemptToResolveRelativeTarget) {
this.attemptToResolveRelativeTarget = false;
const relativeParent = this.getClosestProjectingParent();
if (relativeParent &&
Boolean(relativeParent.resumingFrom) ===
Boolean(this.resumingFrom) &&
!relativeParent.options.layoutScroll &&
relativeParent.target &&
this.animationProgress !== 1) {
this.relativeParent = relativeParent;
this.forceRelativeParentToResolveTarget();
this.relativeTarget = indexLegacy.createBox();
this.relativeTargetOrigin = indexLegacy.createBox();
calcRelativePosition(this.relativeTargetOrigin, this.target, relativeParent.target);
copyBoxInto(this.relativeTarget, this.relativeTargetOrigin);
}
else {
this.relativeParent = this.relativeTarget = undefined;
}
}
/**
* Increase debug counter for resolved target deltas
*/
projectionFrameData.resolvedTargetDeltas++;
}
getClosestProjectingParent() {
if (!this.parent ||
indexLegacy.hasScale(this.parent.latestValues) ||
indexLegacy.has2DTranslate(this.parent.latestValues)) {
return undefined;
}
if (this.parent.isProjecting()) {
return this.parent;
}
else {
return this.parent.getClosestProjectingParent();
}
}
isProjecting() {
return Boolean((this.relativeTarget ||
this.targetDelta ||
this.options.layoutRoot) &&
this.layout);
}
calcProjection() {
var _a;
const lead = this.getLead();
const isShared = Boolean(this.resumingFrom) || this !== lead;
let canSkip = true;
/**
* If this is a normal layout animation and neither this node nor its nearest projecting
* is dirty then we can't skip.
*/
if (this.isProjectionDirty || ((_a = this.parent) === null || _a === void 0 ? void 0 : _a.isProjectionDirty)) {
canSkip = false;
}
/**
* If this is a shared layout animation and this node's shared projection is dirty then
* we can't skip.
*/
if (isShared &&
(this.isSharedProjectionDirty || this.isTransformDirty)) {
canSkip = false;
}
/**
* If we have resolved the target this frame we must recalculate the
* projection to ensure it visually represents the internal calculations.
*/
if (this.resolvedRelativeTargetAt === indexLegacy.frameData.timestamp) {
canSkip = false;
}
if (canSkip)
return;
const { layout, layoutId } = this.options;
/**
* If this section of the tree isn't animating we can
* delete our target sources for the following frame.
*/
this.isTreeAnimating = Boolean((this.parent && this.parent.isTreeAnimating) ||
this.currentAnimation ||
this.pendingAnimation);
if (!this.isTreeAnimating) {
this.targetDelta = this.relativeTarget = undefined;
}
if (!this.layout || !(layout || layoutId))
return;
/**
* Reset the corrected box with the latest values from box, as we're then going
* to perform mutative operations on it.
*/
copyBoxInto(this.layoutCorrected, this.layout.layoutBox);
/**
* Record previous tree scales before updating.
*/
const prevTreeScaleX = this.treeScale.x;
const prevTreeScaleY = this.treeScale.y;
/**
* Apply all the parent deltas to this box to produce the corrected box. This
* is the layout box, as it will appear on screen as a result of the transforms of its parents.
*/
indexLegacy.applyTreeDeltas(this.layoutCorrected, this.treeScale, this.path, isShared);
/**
* If this layer needs to perform scale correction but doesn't have a target,
* use the layout as the target.
*/
if (lead.layout &&
!lead.target &&
(this.treeScale.x !== 1 || this.treeScale.y !== 1)) {
lead.target = lead.layout.layoutBox;
}
const { target } = lead;
if (!target) {
/**
* If we don't have a target to project into, but we were previously
* projecting, we want to remove the stored transform and schedule
* a render to ensure the elements reflect the removed transform.
*/
if (this.projectionTransform) {
this.projectionDelta = indexLegacy.createDelta();
this.projectionTransform = "none";
this.scheduleRender();
}
return;
}
if (!this.projectionDelta) {
this.projectionDelta = indexLegacy.createDelta();
this.projectionDeltaWithTransform = indexLegacy.createDelta();
}
const prevProjectionTransform = this.projectionTransform;
/**
* Update the delta between the corrected box and the target box before user-set transforms were applied.
* This will allow us to calculate the corrected borderRadius and boxShadow to compensate
* for our layout reprojection, but still allow them to be scaled correctly by the user.
* It might be that to simplify this we may want to accept that user-set scale is also corrected
* and we wouldn't have to keep and calc both deltas, OR we could support a user setting
* to allow people to choose whether these styles are corrected based on just the
* layout reprojection or the final bounding box.
*/
calcBoxDelta(this.projectionDelta, this.layoutCorrected, target, this.latestValues);
this.projectionTransform = buildProjectionTransform(this.projectionDelta, this.treeScale);
if (this.projectionTransform !== prevProjectionTransform ||
this.treeScale.x !== prevTreeScaleX ||
this.treeScale.y !== prevTreeScaleY) {
this.hasProjected = true;
this.scheduleRender();
this.notifyListeners("projectionUpdate", target);
}
/**
* Increase debug counter for recalculated projections
*/
projectionFrameData.recalculatedProjection++;
}
hide() {
this.isVisible = false;
// TODO: Schedule render
}
show() {
this.isVisible = true;
// TODO: Schedule render
}
scheduleRender(notifyAll = true) {
this.options.scheduleRender && this.options.scheduleRender();
if (notifyAll) {
const stack = this.getStack();
stack && stack.scheduleRender();
}
if (this.resumingFrom && !this.resumingFrom.instance) {
this.resumingFrom = undefined;
}
}
setAnimationOrigin(delta, hasOnlyRelativeTargetChanged = false) {
const snapshot = this.snapshot;
const snapshotLatestValues = snapshot
? snapshot.latestValues
: {};
const mixedValues = { ...this.latestValues };
const targetDelta = indexLegacy.createDelta();
if (!this.relativeParent ||
!this.relativeParent.options.layoutRoot) {
this.relativeTarget = this.relativeTargetOrigin = undefined;
}
this.attemptToResolveRelativeTarget = !hasOnlyRelativeTargetChanged;
const relativeLayout = indexLegacy.createBox();
const snapshotSource = snapshot ? snapshot.source : undefined;
const layoutSource = this.layout ? this.layout.source : undefined;
const isSharedLayoutAnimation = snapshotSource !== layoutSource;
const stack = this.getStack();
const isOnlyMember = !stack || stack.members.length <= 1;
const shouldCrossfadeOpacity = Boolean(isSharedLayoutAnimation &&
!isOnlyMember &&
this.options.crossfade === true &&
!this.path.some(hasOpacityCrossfade));
this.animationProgress = 0;
let prevRelativeTarget;
this.mixTargetDelta = (latest) => {
const progress = latest / 1000;
mixAxisDelta(targetDelta.x, delta.x, progress);
mixAxisDelta(targetDelta.y, delta.y, progress);
this.setTargetDelta(targetDelta);
if (this.relativeTarget &&
this.relativeTargetOrigin &&
this.layout &&
this.relativeParent &&
this.relativeParent.layout) {
calcRelativePosition(relativeLayout, this.layout.layoutBox, this.relativeParent.layout.layoutBox);
mixBox(this.relativeTarget, this.relativeTargetOrigin, relativeLayout, progress);
/**
* If this is an unchanged relative target we can consider the
* projection not dirty.
*/
if (prevRelativeTarget &&
boxEquals(this.relativeTarget, prevRelativeTarget)) {
this.isProjectionDirty = false;
}
if (!prevRelativeTarget)
prevRelativeTarget = indexLegacy.createBox();
copyBoxInto(prevRelativeTarget, this.relativeTarget);
}
if (isSharedLayoutAnimation) {
this.animationValues = mixedValues;
mixValues(mixedValues, snapshotLatestValues, this.latestValues, progress, shouldCrossfadeOpacity, isOnlyMember);
}
this.root.scheduleUpdateProjection();
this.scheduleRender();
this.animationProgress = progress;
};
this.mixTargetDelta(this.options.layoutRoot ? 1000 : 0);
}
startAnimation(options) {
this.notifyListeners("animationStart");
this.currentAnimation && this.currentAnimation.stop();
if (this.resumingFrom && this.resumingFrom.currentAnimation) {
this.resumingFrom.currentAnimation.stop();
}
if (this.pendingAnimation) {
indexLegacy.cancelFrame(this.pendingAnimation);
this.pendingAnimation = undefined;
}
/**
* Start the animation in the next frame to have a frame with progress 0,
* where the target is the same as when the animation started, so we can
* calculate the relative positions correctly for instant transitions.
*/
this.pendingAnimation = indexLegacy.frame.update(() => {
globalProjectionState.hasAnimatedSinceResize = true;
this.currentAnimation = indexLegacy.animateSingleValue(0, animationTarget, {
...options,
onUpdate: (latest) => {
this.mixTargetDelta(latest);
options.onUpdate && options.onUpdate(latest);
},
onComplete: () => {
options.onComplete && options.onComplete();
this.completeAnimation();
},
});
if (this.resumingFrom) {
this.resumingFrom.currentAnimation = this.currentAnimation;
}
this.pendingAnimation = undefined;
});
}
completeAnimation() {
if (this.resumingFrom) {
this.resumingFrom.currentAnimation = undefined;
this.resumingFrom.preserveOpacity = undefined;
}
const stack = this.getStack();
stack && stack.exitAnimationComplete();
this.resumingFrom =
this.currentAnimation =
this.animationValues =
undefined;
this.notifyListeners("animationComplete");
}
finishAnimation() {
if (this.currentAnimation) {
this.mixTargetDelta && this.mixTargetDelta(animationTarget);
this.currentAnimation.stop();
}
this.completeAnimation();
}
applyTransformsToTarget() {
const lead = this.getLead();
let { targetWithTransforms, target, layout, latestValues } = lead;
if (!targetWithTransforms || !target || !layout)
return;
/**
* If we're only animating position, and this element isn't the lead element,
* then instead of projecting into the lead box we instead want to calculate
* a new target that aligns the two boxes but maintains the layout shape.
*/
if (this !== lead &&
this.layout &&
layout &&
shouldAnimatePositionOnly(this.options.animationType, this.layout.layoutBox, layout.layoutBox)) {
target = this.target || indexLegacy.createBox();
const xLength = calcLength(this.layout.layoutBox.x);
target.x.min = lead.target.x.min;
target.x.max = target.x.min + xLength;
const yLength = calcLength(this.layout.layoutBox.y);
target.y.min = lead.target.y.min;
target.y.max = target.y.min + yLength;
}
copyBoxInto(targetWithTransforms, target);
/**
* Apply the latest user-set transforms to the targetBox to produce the targetBoxFinal.
* This is the final box that we will then project into by calculating a transform delta and
* applying it to the corrected box.
*/
indexLegacy.transformBox(targetWithTransforms, latestValues);
/**
* Update the delta between the corrected box and the final target box, after
* user-set transforms are applied to it. This will be used by the renderer to
* create a transform style that will reproject the element from its layout layout
* into the desired bounding box.
*/
calcBoxDelta(this.projectionDeltaWithTransform, this.layoutCorrected, targetWithTransforms, latestValues);
}
registerSharedNode(layoutId, node) {
if (!this.sharedNodes.has(layoutId)) {
this.sharedNodes.set(layoutId, new NodeStack());
}
const stack = this.sharedNodes.get(layoutId);
stack.add(node);
const config = node.options.initialPromotionConfig;
node.promote({
transition: config ? config.transition : undefined,
preserveFollowOpacity: config && config.shouldPreserveFollowOpacity
? config.shouldPreserveFollowOpacity(node)
: undefined,
});
}
isLead() {
const stack = this.getStack();
return stack ? stack.lead === this : true;
}
getLead() {
var _a;
const { layoutId } = this.options;
return layoutId ? ((_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.lead) || this : this;
}
getPrevLead() {
var _a;
const { layoutId } = this.options;
return layoutId ? (_a = this.getStack()) === null || _a === void 0 ? void 0 : _a.prevLead : undefined;
}
getStack() {
const { layoutId } = this.options;
if (layoutId)
return this.root.sharedNodes.get(layoutId);
}
promote({ needsReset, transition, preserveFollowOpacity, } = {}) {
const stack = this.getStack();
if (stack)
stack.promote(this, preserveFollowOpacity);
if (needsReset) {
this.projectionDelta = undefined;
this.needsReset = true;
}
if (transition)
this.setOptions({ transition });
}
relegate() {
const stack = this.getStack();
if (stack) {
return stack.relegate(this);
}
else {
return false;
}
}
resetRotation() {
const { visualElement } = this.options;
if (!visualElement)
return;
// If there's no detected rotation values, we can early return without a forced render.
let hasRotate = false;
/**
* An unrolled check for rotation values. Most elements don't have any rotation and
* skipping the nested loop and new object creation is 50% faster.
*/
const { latestValues } = visualElement;
if (latestValues.rotate ||
latestValues.rotateX ||
latestValues.rotateY ||
latestValues.rotateZ) {
hasRotate = true;
}
// If there's no rotation values, we don't need to do any more.
if (!hasRotate)
return;
const resetValues = {};
// Check the rotate value of all axes and reset to 0
for (let i = 0; i < transformAxes.length; i++) {
const key = "rotate" + transformAxes[i];
// Record the rotation and then temporarily set it to 0
if (latestValues[key]) {
resetValues[key] = latestValues[key];
visualElement.setStaticValue(key, 0);
}
}
// Force a render of this element to apply the transform with all rotations
// set to 0.
visualElement.render();
// Put back all the values we reset
for (const key in resetValues) {
visualElement.setStaticValue(key, resetValues[key]);
}
// Schedule a render for the next frame. This ensures we won't visually
// see the element with the reset rotate value applied.
visualElement.scheduleRender();
}
getProjectionStyles(styleProp) {
var _a, _b;
if (!this.instance || this.isSVG)
return undefined;
if (!this.isVisible) {
return hiddenVisibility;
}
const styles = {
visibility: "",
};
const transformTemplate = this.getTransformTemplate();
if (this.needsReset) {
this.needsReset = false;
styles.opacity = "";
styles.pointerEvents =
resolveMotionValue(styleProp === null || styleProp === void 0 ? void 0 : styleProp.pointerEvents) || "";
styles.transform = transformTemplate
? transformTemplate(this.latestValues, "")
: "none";
return styles;
}
const lead = this.getLead();
if (!this.projectionDelta || !this.layout || !lead.target) {
const emptyStyles = {};
if (this.options.layoutId) {
emptyStyles.opacity =
this.latestValues.opacity !== undefined
? this.latestValues.opacity
: 1;
emptyStyles.pointerEvents =
resolveMotionValue(styleProp === null || styleProp === void 0 ? void 0 : styleProp.pointerEvents) || "";
}
if (this.hasProjected && !indexLegacy.hasTransform(this.latestValues)) {
emptyStyles.transform = transformTemplate
? transformTemplate({}, "")
: "none";
this.hasProjected = false;
}
return emptyStyles;
}
const valuesToRender = lead.animationValues || lead.latestValues;
this.applyTransformsToTarget();
styles.transform = buildProjectionTransform(this.projectionDeltaWithTransform, this.treeScale, valuesToRender);
if (transformTemplate) {
styles.transform = transformTemplate(valuesToRender, styles.transform);
}
const { x, y } = this.projectionDelta;
styles.transformOrigin = `${x.origin * 100}% ${y.origin * 100}% 0`;
if (lead.animationValues) {
/**
* If the lead component is animating, assign this either the entering/leaving
* opacity
*/
styles.opacity =
lead === this
? (_b = (_a = valuesToRender.opacity) !== null && _a !== void 0 ? _a : this.latestValues.opacity) !== null && _b !== void 0 ? _b : 1
: this.preserveOpacity
? this.latestValues.opacity
: valuesToRender.opacityExit;
}
else {
/**
* Or we're not animating at all, set the lead component to its layout
* opacity and other components to hidden.
*/
styles.opacity =
lead === this
? valuesToRender.opacity !== undefined
? valuesToRender.opacity
: ""
: valuesToRender.opacityExit !== undefined
? valuesToRender.opacityExit
: 0;
}
/**
* Apply scale correction
*/
for (const key in indexLegacy.scaleCorrectors) {
if (valuesToRender[key] === undefined)
continue;
const { correct, applyTo } = indexLegacy.scaleCorrectors[key];
/**
* Only apply scale correction to the value if we have an
* active projection transform. Otherwise these values become
* vulnerable to distortion if the element changes size without
* a corresponding layout animation.
*/
const corrected = styles.transform === "none"
? valuesToRender[key]
: correct(valuesToRender[key], lead);
if (applyTo) {
const num = applyTo.length;
for (let i = 0; i < num; i++) {
styles[applyTo[i]] = corrected;
}
}
else {
styles[key] = corrected;
}
}
/**
* Disable pointer events on follow components. This is to ensure
* that if a follow component covers a lead component it doesn't block
* pointer events on the lead.
*/
if (this.options.layoutId) {
styles.pointerEvents =
lead === this
? resolveMotionValue(styleProp === null || styleProp === void 0 ? void 0 : styleProp.pointerEvents) || ""
: "none";
}
return styles;
}
clearSnapshot() {
this.resumeFrom = this.snapshot = undefined;
}
// Only run on root
resetTree() {
this.root.nodes.forEach((node) => { var _a; return (_a = node.currentAnimation) === null || _a === void 0 ? void 0 : _a.stop(); });
this.root.nodes.forEach(clearMeasurements);
this.root.sharedNodes.clear();
}
};
}
function updateLayout(node) {
node.updateLayout();
}
function notifyLayoutUpdate(node) {
var _a;
const snapshot = ((_a = node.resumeFrom) === null || _a === void 0 ? void 0 : _a.snapshot) || node.snapshot;
if (node.isLead() &&
node.layout &&
snapshot &&
node.hasListeners("didUpdate")) {
const { layoutBox: layout, measuredBox: measuredLayout } = node.layout;
const { animationType } = node.options;
const isShared = snapshot.source !== node.layout.source;
// TODO Maybe we want to also resize the layout snapshot so we don't trigger
// animations for instance if layout="size" and an element has only changed position
if (animationType === "size") {
eachAxis((axis) => {
const axisSnapshot = isShared
? snapshot.measuredBox[axis]
: snapshot.layoutBox[axis];
const length = calcLength(axisSnapshot);
axisSnapshot.min = layout[axis].min;
axisSnapshot.max = axisSnapshot.min + length;
});
}
else if (shouldAnimatePositionOnly(animationType, snapshot.layoutBox, layout)) {
eachAxis((axis) => {
const axisSnapshot = isShared
? snapshot.measuredBox[axis]
: snapshot.layoutBox[axis];
const length = calcLength(layout[axis]);
axisSnapshot.max = axisSnapshot.min + length;
/**
* Ensure relative target gets resized and rerendererd
*/
if (node.relativeTarget && !node.currentAnimation) {
node.isProjectionDirty = true;
node.relativeTarget[axis].max =
node.relativeTarget[axis].min + length;
}
});
}
const layoutDelta = indexLegacy.createDelta();
calcBoxDelta(layoutDelta, layout, snapshot.layoutBox);
const visualDelta = indexLegacy.createDelta();
if (isShared) {
calcBoxDelta(visualDelta, node.applyTransform(measuredLayout, true), snapshot.measuredBox);
}
else {
calcBoxDelta(visualDelta, layout, snapshot.layoutBox);
}
const hasLayoutChanged = !isDeltaZero(layoutDelta);
let hasRelativeTargetChanged = false;
if (!node.resumeFrom) {
const relativeParent = node.getClosestProjectingParent();
/**
* If the relativeParent is itself resuming from a different element then
* the relative snapshot is not relavent
*/
if (relativeParent && !relativeParent.resumeFrom) {
const { snapshot: parentSnapshot, layout: parentLayout } = relativeParent;
if (parentSnapshot && parentLayout) {
const relativeSnapshot = indexLegacy.createBox();
calcRelativePosition(relativeSnapshot, snapshot.layoutBox, parentSnapshot.layoutBox);
const relativeLayout = indexLegacy.createBox();
calcRelativePosition(relativeLayout, layout, parentLayout.layoutBox);
if (!boxEqualsRounded(relativeSnapshot, relativeLayout)) {
hasRelativeTargetChanged = true;
}
if (relativeParent.options.layoutRoot) {
node.relativeTarget = relativeLayout;
node.relativeTargetOrigin = relativeSnapshot;
node.relativeParent = relativeParent;
}
}
}
}
node.notifyListeners("didUpdate", {
layout,
snapshot,
delta: visualDelta,
layoutDelta,
hasLayoutChanged,
hasRelativeTargetChanged,
});
}
else if (node.isLead()) {
const { onExitComplete } = node.options;
onExitComplete && onExitComplete();
}
/**
* Clearing transition
* TODO: Investigate why this transition is being passed in as {type: false } from Framer
* and why we need it at all
*/
node.options.transition = undefined;
}
function propagateDirtyNodes(node) {
/**
* Increase debug counter for nodes encountered this frame
*/
projectionFrameData.totalNodes++;
if (!node.parent)
return;
/**
* If this node isn't projecting, propagate isProjectionDirty. It will have
* no performance impact but it will allow the next child that *is* projecting
* but *isn't* dirty to just check its parent to see if *any* ancestor needs
* correcting.
*/
if (!node.isProjecting()) {
node.isProjectionDirty = node.parent.isProjectionDirty;
}
/**
* Propagate isSharedProjectionDirty and isTransformDirty
* throughout the whole tree. A future revision can take another look at
* this but for safety we still recalcualte shared nodes.
*/
node.isSharedProjectionDirty || (node.isSharedProjectionDirty = Boolean(node.isProjectionDirty ||
node.parent.isProjectionDirty ||
node.parent.isSharedProjectionDirty));
node.isTransformDirty || (node.isTransformDirty = node.parent.isTransformDirty);
}
function cleanDirtyNodes(node) {
node.isProjectionDirty =
node.isSharedProjectionDirty =
node.isTransformDirty =
false;
}
function clearSnapshot(node) {
node.clearSnapshot();
}
function clearMeasurements(node) {
node.clearMeasurements();
}
function clearIsLayoutDirty(node) {
node.isLayoutDirty = false;
}
function resetTransformStyle(node) {
const { visualElement } = node.options;
if (visualElement && visualElement.getProps().onBeforeLayoutMeasure) {
visualElement.notify("BeforeLayoutMeasure");
}
node.resetTransform();
}
function finishAnimation(node) {
node.finishAnimation();
node.targetDelta = node.relativeTarget = node.target = undefined;
node.isProjectionDirty = true;
}
function resolveTargetDelta(node) {
node.resolveTargetDelta();
}
function calcProjection(node) {
node.calcProjection();
}
function resetRotation(node) {
node.resetRotation();
}
function removeLeadSnapshots(stack) {
stack.removeLeadSnapshot();
}
function mixAxisDelta(output, delta, p) {
output.translate = indexLegacy.mix(delta.translate, 0, p);
output.scale = indexLegacy.mix(delta.scale, 1, p);
output.origin = delta.origin;
output.originPoint = delta.originPoint;
}
function mixAxis(output, from, to, p) {
output.min = indexLegacy.mix(from.min, to.min, p);
output.max = indexLegacy.mix(from.max, to.max, p);
}
function mixBox(output, from, to, p) {
mixAxis(output.x, from.x, to.x, p);
mixAxis(output.y, from.y, to.y, p);
}
function hasOpacityCrossfade(node) {
return (node.animationValues && node.animationValues.opacityExit !== undefined);
}
const defaultLayoutTransition = {
duration: 0.45,
ease: [0.4, 0, 0.1, 1],
};
const userAgentContains = (string) => typeof navigator !== "undefined" &&
navigator.userAgent.toLowerCase().includes(string);
/**
* Measured bounding boxes must be rounded in Safari and
* left untouched in Chrome, otherwise non-integer layouts within scaled-up elements
* can appear to jump.
*/
const roundPoint = userAgentContains("applewebkit/") && !userAgentContains("chrome/")
? Math.round
: indexLegacy.noop;
function roundAxis(axis) {
// Round to the nearest .5 pixels to support subpixel layouts
axis.min = roundPoint(axis.min);
axis.max = roundPoint(axis.max);
}
function roundBox(box) {
roundAxis(box.x);
roundAxis(box.y);
}
function shouldAnimatePositionOnly(animationType, snapshot, layout) {
return (animationType === "position" ||
(animationType === "preserve-aspect" &&
!isNear(aspectRatio(snapshot), aspectRatio(layout), 0.2)));
}
const DocumentProjectionNode = createProjectionNode({
attachResizeListener: (ref, notify) => addDomEvent(ref, "resize", notify),
measureScroll: () => ({
x: document.documentElement.scrollLeft || document.body.scrollLeft,
y: document.documentElement.scrollTop || document.body.scrollTop,
}),
checkIsScrollRoot: () => true,
});
const rootProjectionNode = {
current: undefined,
};
const HTMLProjectionNode = createProjectionNode({
measureScroll: (instance) => ({
x: instance.scrollLeft,
y: instance.scrollTop,
}),
defaultParent: () => {
if (!rootProjectionNode.current) {
const documentNode = new DocumentProjectionNode({});
documentNode.mount(window);
documentNode.setOptions({ layoutScroll: true });
rootProjectionNode.current = documentNode;
}
return rootProjectionNode.current;
},
resetTransform: (instance, value) => {
instance.style.transform = value !== undefined ? value : "none";
},
checkIsScrollRoot: (instance) => Boolean(window.getComputedStyle(instance).position === "fixed"),
});
const notify = (node) => !node.isLayoutDirty && node.willUpdate(false);
function nodeGroup() {
const nodes = new Set();
const subscriptions = new WeakMap();
const dirtyAll = () => nodes.forEach(notify);
return {
add: (node) => {
nodes.add(node);
subscriptions.set(node, node.addEventListener("willUpdate", dirtyAll));
},
remove: (node) => {
nodes.delete(node);
const unsubscribe = subscriptions.get(node);
if (unsubscribe) {
unsubscribe();
subscriptions.delete(node);
}
dirtyAll();
},
dirty: dirtyAll,
};
}
function pixelsToPercent(pixels, axis) {
if (axis.max === axis.min)
return 0;
return (pixels / (axis.max - axis.min)) * 100;
}
/**
* We always correct borderRadius as a percentage rather than pixels to reduce paints.
* For example, if you are projecting a box that is 100px wide with a 10px borderRadius
* into a box that is 200px wide with a 20px borderRadius, that is actually a 10%
* borderRadius in both states. If we animate between the two in pixels that will trigger
* a paint each time. If we animate between the two in percentage we'll avoid a paint.
*/
const correctBorderRadius = {
correct: (latest, node) => {
if (!node.target)
return latest;
/**
* If latest is a string, if it's a percentage we can return immediately as it's
* going to be stretched appropriately. Otherwise, if it's a pixel, convert it to a number.
*/
if (typeof latest === "string") {
if (indexLegacy.px.test(latest)) {
latest = parseFloat(latest);
}
else {
return latest;
}
}
/**
* If latest is a number, it's a pixel value. We use the current viewportBox to calculate that
* pixel value as a percentage of each axis
*/
const x = pixelsToPercent(latest, node.target.x);
const y = pixelsToPercent(latest, node.target.y);
return `${x}% ${y}%`;
},
};
const correctBoxShadow = {
correct: (latest, { treeScale, projectionDelta }) => {
const original = latest;
const shadow = indexLegacy.complex.parse(latest);
// TODO: Doesn't support multiple shadows
if (shadow.length > 5)
return original;
const template = indexLegacy.complex.createTransformer(latest);
const offset = typeof shadow[0] !== "number" ? 1 : 0;
// Calculate the overall context scale
const xScale = projectionDelta.x.scale * treeScale.x;
const yScale = projectionDelta.y.scale * treeScale.y;
shadow[0 + offset] /= xScale;
shadow[1 + offset] /= yScale;
/**
* Ideally we'd correct x and y scales individually, but because blur and
* spread apply to both we have to take a scale average and apply that instead.
* We could potentially improve the outcome of this by incorporating the ratio between
* the two scales.
*/
const averageScale = indexLegacy.mix(xScale, yScale, 0.5);
// Blur
if (typeof shadow[2 + offset] === "number")
shadow[2 + offset] /= averageScale;
// Spread
if (typeof shadow[3 + offset] === "number")
shadow[3 + offset] /= averageScale;
return template(shadow);
},
};
/**
* 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() {
const context = React.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.useId();
React.useEffect(() => register(id), []);
const safeToRemove = () => onExitComplete && onExitComplete(id);
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.useContext(PresenceContext));
}
function isPresent(context) {
return context === null ? true : context.isPresent;
}
class MeasureLayoutWithContext extends React__default["default"].Component {
/**
* This only mounts projection nodes for components that
* need measuring, we might want to do it for all components
* in order to incorporate transforms
*/
componentDidMount() {
const { visualElement, layoutGroup, switchLayoutGroup, layoutId } = this.props;
const { projection } = visualElement;
indexLegacy.addScaleCorrector(defaultScaleCorrectors);
if (projection) {
if (layoutGroup.group)
layoutGroup.group.add(projection);
if (switchLayoutGroup && switchLayoutGroup.register && layoutId) {
switchLayoutGroup.register(projection);
}
projection.root.didUpdate();
projection.addEventListener("animationComplete", () => {
this.safeToRemove();
});
projection.setOptions({
...projection.options,
onExitComplete: () => this.safeToRemove(),
});
}
globalProjectionState.hasEverUpdated = true;
}
getSnapshotBeforeUpdate(prevProps) {
const { layoutDependency, visualElement, drag, isPresent } = this.props;
const projection = visualElement.projection;
if (!projection)
return null;
/**
* TODO: We use this data in relegate to determine whether to
* promote a previous element. There's no guarantee its presence data
* will have updated by this point - if a bug like this arises it will
* have to be that we markForRelegation and then find a new lead some other way,
* perhaps in didUpdate
*/
projection.isPresent = isPresent;
if (drag ||
prevProps.layoutDependency !== layoutDependency ||
layoutDependency === undefined) {
projection.willUpdate();
}
else {
this.safeToRemove();
}
if (prevProps.isPresent !== isPresent) {
if (isPresent) {
projection.promote();
}
else if (!projection.relegate()) {
/**
* If there's another stack member taking over from this one,
* it's in charge of the exit animation and therefore should
* be in charge of the safe to remove. Otherwise we call it here.
*/
indexLegacy.frame.postRender(() => {
const stack = projection.getStack();
if (!stack || !stack.members.length) {
this.safeToRemove();
}
});
}
}
return null;
}
componentDidUpdate() {
const { projection } = this.props.visualElement;
if (projection) {
projection.root.didUpdate();
queueMicrotask(() => {
if (!projection.currentAnimation && projection.isLead()) {
this.safeToRemove();
}
});
}
}
componentWillUnmount() {
const { visualElement, layoutGroup, switchLayoutGroup: promoteContext, } = this.props;
const { projection } = visualElement;
if (projection) {
projection.scheduleCheckAfterUnmount();
if (layoutGroup && layoutGroup.group)
layoutGroup.group.remove(projection);
if (promoteContext && promoteContext.deregister)
promoteContext.deregister(projection);
}
}
safeToRemove() {
const { safeToRemove } = this.props;
safeToRemove && safeToRemove();
}
render() {
return null;
}
}
function MeasureLayout(props) {
const [isPresent, safeToRemove] = usePresence();
const layoutGroup = React.useContext(LayoutGroupContext);
return (React__default["default"].createElement(MeasureLayoutWithContext, { ...props, layoutGroup: layoutGroup, switchLayoutGroup: React.useContext(SwitchLayoutGroupContext), isPresent: isPresent, safeToRemove: safeToRemove }));
}
const defaultScaleCorrectors = {
borderRadius: {
...correctBorderRadius,
applyTo: [
"borderTopLeftRadius",
"borderTopRightRadius",
"borderBottomLeftRadius",
"borderBottomRightRadius",
],
},
borderTopLeftRadius: correctBorderRadius,
borderTopRightRadius: correctBorderRadius,
borderBottomLeftRadius: correctBorderRadius,
borderBottomRightRadius: correctBorderRadius,
boxShadow: correctBoxShadow,
};
const drag = {
pan: {
Feature: PanGesture,
},
drag: {
Feature: DragGesture,
ProjectionNode: HTMLProjectionNode,
MeasureLayout,
},
};
const createDomVisualElement = (Component, options) => {
return isSVGComponent(Component)
? new indexLegacy.SVGVisualElement(options, { enableHardwareAcceleration: false })
: new indexLegacy.HTMLVisualElement(options, { enableHardwareAcceleration: true });
};
const layout = {
layout: {
ProjectionNode: HTMLProjectionNode,
MeasureLayout,
},
};
const preloadedFeatures = {
...animations,
...gestureAnimations,
...drag,
...layout,
};
/**
* HTML & SVG components, optimised for use with gestures and animation. These can be used as
* drop-in replacements for any HTML & SVG component, all CSS & SVG properties are supported.
*
* @public
*/
const motion = /*@__PURE__*/ createMotionProxy((Component, config) => createDomMotionConfig(Component, config, preloadedFeatures, createDomVisualElement));
/**
* Create a DOM `motion` component with the provided string. This is primarily intended
* as a full alternative to `motion` for consumers who have to support environments that don't
* support `Proxy`.
*
* ```javascript
* import { createDomMotionComponent } from "framer-motion"
*
* const motion = {
* div: createDomMotionComponent('div')
* }
* ```
*
* @public
*/
function createDomMotionComponent(key) {
return createMotionComponent(createDomMotionConfig(key, { forwardMotionProps: false }, preloadedFeatures, createDomVisualElement));
}
/**
* @public
*/
const m = createMotionProxy(createDomMotionConfig);
function useIsMounted() {
const isMounted = React.useRef(false);
useIsomorphicLayoutEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
function useForceUpdate() {
const isMounted = useIsMounted();
const [forcedRenderCount, setForcedRenderCount] = React.useState(0);
const forceRender = React.useCallback(() => {
isMounted.current && setForcedRenderCount(forcedRenderCount + 1);
}, [forcedRenderCount]);
/**
* Defer this to the end of the next animation frame in case there are multiple
* synchronous calls.
*/
const deferredForceRender = React.useCallback(() => indexLegacy.frame.postRender(forceRender), [forceRender]);
return [deferredForceRender, forcedRenderCount];
}
/**
* 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 size = this.props.sizeRef.current;
size.height = element.offsetHeight || 0;
size.width = element.offsetWidth || 0;
size.top = element.offsetTop;
size.left = element.offsetLeft;
}
return null;
}
/**
* Required with getSnapshotBeforeUpdate to stop React complaining.
*/
componentDidUpdate() { }
render() {
return this.props.children;
}
}
function PopChild({ children, isPresent }) {
const id = React.useId();
const ref = React.useRef(null);
const size = React.useRef({
width: 0,
height: 0,
top: 0,
left: 0,
});
/**
* 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.useInsertionEffect(() => {
const { width, height, top, left } = size.current;
if (isPresent || !ref.current || !width || !height)
return;
ref.current.dataset.motionPopId = id;
const style = document.createElement("style");
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;
top: ${top}px !important;
left: ${left}px !important;
}
`);
}
return () => {
document.head.removeChild(style);
};
}, [isPresent]);
return (React__namespace.createElement(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size }, React__namespace.cloneElement(children, { ref })));
}
const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, }) => {
const presenceChildren = useConstant(newChildrenMap);
const id = React.useId();
const context = React.useMemo(() => ({
id,
initial,
isPresent,
custom,
onExitComplete: (childId) => {
presenceChildren.set(childId, true);
for (const isComplete of presenceChildren.values()) {
if (!isComplete)
return; // can stop searching when any is incomplete
}
onExitComplete && onExitComplete();
},
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 ? undefined : [isPresent]);
React.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 = React__namespace.createElement(PopChild, { isPresent: isPresent }, children);
}
return (React__namespace.createElement(PresenceContext.Provider, { value: context }, children));
};
function newChildrenMap() {
return new Map();
}
function useUnmountEffect(callback) {
return React.useEffect(() => () => callback(), []);
}
const getChildKey = (child) => child.key || "";
function updateChildLookup(children, allChildren) {
children.forEach((child) => {
const key = getChildKey(child);
allChildren.set(key, child);
});
}
function onlyElements(children) {
const filtered = [];
// We use forEach here instead of map as map mutates the component key by preprending `.$`
React.Children.forEach(children, (child) => {
if (React.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, exitBeforeEnter, presenceAffectsLayout = true, mode = "sync", }) => {
indexLegacy.invariant(!exitBeforeEnter, "Replace exitBeforeEnter with mode='wait'");
// We want to force a re-render once all exiting animations have finished. We
// either use a local forceRender function, or one from a parent context if it exists.
const forceRender = React.useContext(LayoutGroupContext).forceRender || useForceUpdate()[0];
const isMounted = useIsMounted();
// Filter out any children that aren't ReactElements. We can only track ReactElements with a props.key
const filteredChildren = onlyElements(children);
let childrenToRender = filteredChildren;
const exitingChildren = React.useRef(new Map()).current;
// Keep a living record of the children we're actually rendering so we
// can diff to figure out which are entering and exiting
const presentChildren = React.useRef(childrenToRender);
// A lookup table to quickly reference components by key
const allChildren = React.useRef(new Map()).current;
// If this is the initial component render, just deal with logic surrounding whether
// we play onMount animations or not.
const isInitialRender = React.useRef(true);
useIsomorphicLayoutEffect(() => {
isInitialRender.current = false;
updateChildLookup(filteredChildren, allChildren);
presentChildren.current = childrenToRender;
});
useUnmountEffect(() => {
isInitialRender.current = true;
allChildren.clear();
exitingChildren.clear();
});
if (isInitialRender.current) {
return (React__namespace.createElement(React__namespace.Fragment, null, childrenToRender.map((child) => (React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, initial: initial ? undefined : false, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child)))));
}
// If this is a subsequent render, deal with entering and exiting children
childrenToRender = [...childrenToRender];
// Diff the keys of the currently-present and target children to update our
// exiting list.
const presentKeys = presentChildren.current.map(getChildKey);
const targetKeys = filteredChildren.map(getChildKey);
// Diff the present children with our target children and mark those that are exiting
const numPresent = presentKeys.length;
for (let i = 0; i < numPresent; i++) {
const key = presentKeys[i];
if (targetKeys.indexOf(key) === -1 && !exitingChildren.has(key)) {
exitingChildren.set(key, undefined);
}
}
// If we currently have exiting children, and we're deferring rendering incoming children
// until after all current children have exiting, empty the childrenToRender array
if (mode === "wait" && exitingChildren.size) {
childrenToRender = [];
}
// Loop through all currently exiting components and clone them to overwrite `animate`
// with any `exit` prop they might have defined.
exitingChildren.forEach((component, key) => {
// If this component is actually entering again, early return
if (targetKeys.indexOf(key) !== -1)
return;
const child = allChildren.get(key);
if (!child)
return;
const insertionIndex = presentKeys.indexOf(key);
let exitingComponent = component;
if (!exitingComponent) {
const onExit = () => {
// clean up the exiting children map
exitingChildren.delete(key);
// compute the keys of children that were rendered once but are no longer present
// this could happen in case of too many fast consequent renderings
// @link https://github.com/framer/motion/issues/2023
const leftOverKeys = Array.from(allChildren.keys()).filter((childKey) => !targetKeys.includes(childKey));
// clean up the all children map
leftOverKeys.forEach((leftOverKey) => allChildren.delete(leftOverKey));
// make sure to render only the children that are actually visible
presentChildren.current = filteredChildren.filter((presentChild) => {
const presentChildKey = getChildKey(presentChild);
return (
// filter out the node exiting
presentChildKey === key ||
// filter out the leftover children
leftOverKeys.includes(presentChildKey));
});
// Defer re-rendering until all exiting children have indeed left
if (!exitingChildren.size) {
if (isMounted.current === false)
return;
forceRender();
onExitComplete && onExitComplete();
}
};
exitingComponent = (React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: false, onExitComplete: onExit, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child));
exitingChildren.set(key, exitingComponent);
}
childrenToRender.splice(insertionIndex, 0, exitingComponent);
});
// Add `MotionContext` even to children that don't need it to ensure we're rendering
// the same tree between renders
childrenToRender = childrenToRender.map((child) => {
const key = child.key;
return exitingChildren.has(key) ? (child) : (React__namespace.createElement(PresenceChild, { key: getChildKey(child), isPresent: true, presenceAffectsLayout: presenceAffectsLayout, mode: mode }, child));
});
if (process.env.NODE_ENV !== "production" &&
mode === "wait" &&
childrenToRender.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.`);
}
return (React__namespace.createElement(React__namespace.Fragment, null, exitingChildren.size
? childrenToRender
: childrenToRender.map((child) => React.cloneElement(child))));
};
/**
* `MotionConfig` is used to set configuration options for all children `motion` components.
*
* ```jsx
* import { motion, MotionConfig } from "framer-motion"
*
* export function App() {
* return (
* <MotionConfig transition={{ type: "spring" }}>
* <motion.div animate={{ x: 100 }} />
* </MotionConfig>
* )
* }
* ```
*
* @public
*/
function MotionConfig({ children, isValidProp, ...config }) {
isValidProp && loadExternalIsValidProp(isValidProp);
/**
* Inherit props from any parent MotionConfig components
*/
config = { ...React.useContext(MotionConfigContext), ...config };
/**
* Don't allow isStatic to change between renders as it affects how many hooks
* motion components fire.
*/
config.isStatic = useConstant(() => config.isStatic);
/**
* Creating a new config context object will re-render every `motion` component
* every time it renders. So we only want to create a new one sparingly.
*/
const context = React.useMemo(() => config, [JSON.stringify(config.transition), config.transformPagePoint, config.reducedMotion]);
return (React__namespace.createElement(MotionConfigContext.Provider, { value: context }, children));
}
/**
* Used in conjunction with the `m` component to reduce bundle size.
*
* `m` is a version of the `motion` component that only loads functionality
* critical for the initial render.
*
* `LazyMotion` can then be used to either synchronously or asynchronously
* load animation and gesture support.
*
* ```jsx
* // Synchronous loading
* import { LazyMotion, m, domAnimation } from "framer-motion"
*
* function App() {
* return (
* <LazyMotion features={domAnimation}>
* <m.div animate={{ scale: 2 }} />
* </LazyMotion>
* )
* }
*
* // Asynchronous loading
* import { LazyMotion, m } from "framer-motion"
*
* function App() {
* return (
* <LazyMotion features={() => import('./path/to/domAnimation')}>
* <m.div animate={{ scale: 2 }} />
* </LazyMotion>
* )
* }
* ```
*
* @public
*/
function LazyMotion({ children, features, strict = false }) {
const [, setIsLoaded] = React.useState(!isLazyBundle(features));
const loadedRenderer = React.useRef(undefined);
/**
* If this is a synchronous load, load features immediately
*/
if (!isLazyBundle(features)) {
const { renderer, ...loadedFeatures } = features;
loadedRenderer.current = renderer;
loadFeatures(loadedFeatures);
}
React.useEffect(() => {
if (isLazyBundle(features)) {
features().then(({ renderer, ...loadedFeatures }) => {
loadFeatures(loadedFeatures);
loadedRenderer.current = renderer;
setIsLoaded(true);
});
}
}, []);
return (React__namespace.createElement(LazyContext.Provider, { value: { renderer: loadedRenderer.current, strict } }, children));
}
function isLazyBundle(features) {
return typeof features === "function";
}
/**
* Note: Still used by components generated by old versions of Framer
*
* @deprecated
*/
const DeprecatedLayoutGroupContext = React.createContext(null);
const shouldInheritGroup = (inherit) => inherit === true;
const shouldInheritId = (inherit) => shouldInheritGroup(inherit === true) || inherit === "id";
const LayoutGroup = ({ children, id, inherit = true }) => {
const layoutGroupContext = React.useContext(LayoutGroupContext);
const deprecatedLayoutGroupContext = React.useContext(DeprecatedLayoutGroupContext);
const [forceRender, key] = useForceUpdate();
const context = React.useRef(null);
const upstreamId = layoutGroupContext.id || deprecatedLayoutGroupContext;
if (context.current === null) {
if (shouldInheritId(inherit) && upstreamId) {
id = id ? upstreamId + "-" + id : upstreamId;
}
context.current = {
id,
group: shouldInheritGroup(inherit)
? layoutGroupContext.group || nodeGroup()
: nodeGroup(),
};
}
const memoizedContext = React.useMemo(() => ({ ...context.current, forceRender }), [key]);
return (React__namespace.createElement(LayoutGroupContext.Provider, { value: memoizedContext }, children));
};
const ReorderContext = React.createContext(null);
function checkReorder(order, value, offset, velocity) {
if (!velocity)
return order;
const index = order.findIndex((item) => item.value === value);
if (index === -1)
return order;
const nextOffset = velocity > 0 ? 1 : -1;
const nextItem = order[index + nextOffset];
if (!nextItem)
return order;
const item = order[index];
const nextLayout = nextItem.layout;
const nextItemCenter = indexLegacy.mix(nextLayout.min, nextLayout.max, 0.5);
if ((nextOffset === 1 && item.layout.max + offset > nextItemCenter) ||
(nextOffset === -1 && item.layout.min + offset < nextItemCenter)) {
return indexLegacy.moveItem(order, index, index + nextOffset);
}
return order;
}
function ReorderGroup({ children, as = "ul", axis = "y", onReorder, values, ...props }, externalRef) {
const Component = useConstant(() => motion(as));
const order = [];
const isReordering = React.useRef(false);
indexLegacy.invariant(Boolean(values), "Reorder.Group must be provided a values prop");
const context = {
axis,
registerItem: (value, layout) => {
// If the entry was already added, update it rather than adding it again
const idx = order.findIndex((entry) => value === entry.value);
if (idx !== -1) {
order[idx].layout = layout[axis];
}
else {
order.push({ value: value, layout: layout[axis] });
}
order.sort(compareMin);
},
updateOrder: (item, offset, velocity) => {
if (isReordering.current)
return;
const newOrder = checkReorder(order, item, offset, velocity);
if (order !== newOrder) {
isReordering.current = true;
onReorder(newOrder
.map(getValue)
.filter((value) => values.indexOf(value) !== -1));
}
},
};
React.useEffect(() => {
isReordering.current = false;
});
return (React__namespace.createElement(Component, { ...props, ref: externalRef, ignoreStrict: true },
React__namespace.createElement(ReorderContext.Provider, { value: context }, children)));
}
const Group = React.forwardRef(ReorderGroup);
function getValue(item) {
return item.value;
}
function compareMin(a, b) {
return a.layout.min - b.layout.min;
}
/**
* Creates a `MotionValue` to track the state and velocity of a value.
*
* Usually, these are created automatically. For advanced use-cases, like use with `useTransform`, you can create `MotionValue`s externally and pass them into the animated component via the `style` prop.
*
* ```jsx
* export const MyComponent = () => {
* const scale = useMotionValue(1)
*
* return <motion.div style={{ scale }} />
* }
* ```
*
* @param initial - The initial state.
*
* @public
*/
function useMotionValue(initial) {
const value = useConstant(() => indexLegacy.motionValue(initial));
/**
* If this motion value is being used in static mode, like on
* the Framer canvas, force components to rerender when the motion
* value is updated.
*/
const { isStatic } = React.useContext(MotionConfigContext);
if (isStatic) {
const [, setLatest] = React.useState(initial);
React.useEffect(() => value.on("change", setLatest), []);
}
return value;
}
function useCombineMotionValues(values, combineValues) {
/**
* Initialise the returned motion value. This remains the same between renders.
*/
const value = useMotionValue(combineValues());
/**
* Create a function that will update the template motion value with the latest values.
* This is pre-bound so whenever a motion value updates it can schedule its
* execution in Framesync. If it's already been scheduled it won't be fired twice
* in a single frame.
*/
const updateValue = () => value.set(combineValues());
/**
* Synchronously update the motion value with the latest values during the render.
* This ensures that within a React render, the styles applied to the DOM are up-to-date.
*/
updateValue();
/**
* Subscribe to all motion values found within the template. Whenever any of them change,
* schedule an update.
*/
useIsomorphicLayoutEffect(() => {
const scheduleUpdate = () => indexLegacy.frame.update(updateValue, false, true);
const subscriptions = values.map((v) => v.on("change", scheduleUpdate));
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
indexLegacy.cancelFrame(updateValue);
};
});
return value;
}
function useComputed(compute) {
/**
* Open session of collectMotionValues. Any MotionValue that calls get()
* will be saved into this array.
*/
indexLegacy.collectMotionValues.current = [];
compute();
const value = useCombineMotionValues(indexLegacy.collectMotionValues.current, compute);
/**
* Synchronously close session of collectMotionValues.
*/
indexLegacy.collectMotionValues.current = undefined;
return value;
}
function useTransform(input, inputRangeOrTransformer, outputRange, options) {
if (typeof input === "function") {
return useComputed(input);
}
const transformer = typeof inputRangeOrTransformer === "function"
? inputRangeOrTransformer
: indexLegacy.transform(inputRangeOrTransformer, outputRange, options);
return Array.isArray(input)
? useListTransform(input, transformer)
: useListTransform([input], ([latest]) => transformer(latest));
}
function useListTransform(values, transformer) {
const latest = useConstant(() => []);
return useCombineMotionValues(values, () => {
latest.length = 0;
const numValues = values.length;
for (let i = 0; i < numValues; i++) {
latest[i] = values[i].get();
}
return transformer(latest);
});
}
function useDefaultMotionValue(value, defaultValue = 0) {
return indexLegacy.isMotionValue(value) ? value : useMotionValue(defaultValue);
}
function ReorderItem({ children, style = {}, value, as = "li", onDrag, layout = true, ...props }, externalRef) {
const Component = useConstant(() => motion(as));
const context = React.useContext(ReorderContext);
const point = {
x: useDefaultMotionValue(style.x),
y: useDefaultMotionValue(style.y),
};
const zIndex = useTransform([point.x, point.y], ([latestX, latestY]) => latestX || latestY ? 1 : "unset");
indexLegacy.invariant(Boolean(context), "Reorder.Item must be a child of Reorder.Group");
const { axis, registerItem, updateOrder } = context;
return (React__namespace.createElement(Component, { drag: axis, ...props, dragSnapToOrigin: true, style: { ...style, x: point.x, y: point.y, zIndex }, layout: layout, onDrag: (event, gesturePoint) => {
const { velocity } = gesturePoint;
velocity[axis] &&
updateOrder(value, point[axis].get(), velocity[axis]);
onDrag && onDrag(event, gesturePoint);
}, onLayoutMeasure: (measured) => registerItem(value, measured), ref: externalRef, ignoreStrict: true }, children));
}
const Item = React.forwardRef(ReorderItem);
const Reorder = {
Group,
Item,
};
/**
* @public
*/
const domAnimation = {
renderer: createDomVisualElement,
...animations,
...gestureAnimations,
};
/**
* @public
*/
const domMax = {
...domAnimation,
...drag,
...layout,
};
/**
* Combine multiple motion values into a new one using a string template literal.
*
* ```jsx
* import {
* motion,
* useSpring,
* useMotionValue,
* useMotionTemplate
* } from "framer-motion"
*
* function Component() {
* const shadowX = useSpring(0)
* const shadowY = useMotionValue(0)
* const shadow = useMotionTemplate`drop-shadow(${shadowX}px ${shadowY}px 20px rgba(0,0,0,0.3))`
*
* return <motion.div style={{ filter: shadow }} />
* }
* ```
*
* @public
*/
function useMotionTemplate(fragments, ...values) {
/**
* Create a function that will build a string from the latest motion values.
*/
const numFragments = fragments.length;
function buildValue() {
let output = ``;
for (let i = 0; i < numFragments; i++) {
output += fragments[i];
const value = values[i];
if (value) {
output += indexLegacy.isMotionValue(value) ? value.get() : value;
}
}
return output;
}
return useCombineMotionValues(values.filter(indexLegacy.isMotionValue), buildValue);
}
/**
* Creates a `MotionValue` that, when `set`, will use a spring animation to animate to its new state.
*
* It can either work as a stand-alone `MotionValue` by initialising it with a value, or as a subscriber
* to another `MotionValue`.
*
* @remarks
*
* ```jsx
* const x = useSpring(0, { stiffness: 300 })
* const y = useSpring(x, { damping: 10 })
* ```
*
* @param inputValue - `MotionValue` or number. If provided a `MotionValue`, when the input `MotionValue` changes, the created `MotionValue` will spring towards that value.
* @param springConfig - Configuration options for the spring.
* @returns `MotionValue`
*
* @public
*/
function useSpring(source, config = {}) {
const { isStatic } = React.useContext(MotionConfigContext);
const activeSpringAnimation = React.useRef(null);
const value = useMotionValue(indexLegacy.isMotionValue(source) ? source.get() : source);
const stopAnimation = () => {
if (activeSpringAnimation.current) {
activeSpringAnimation.current.stop();
}
};
React.useInsertionEffect(() => {
return value.attach((v, set) => {
/**
* A more hollistic approach to this might be to use isStatic to fix VisualElement animations
* at that level, but this will work for now
*/
if (isStatic)
return set(v);
stopAnimation();
activeSpringAnimation.current = indexLegacy.animateValue({
keyframes: [value.get(), v],
velocity: value.getVelocity(),
type: "spring",
restDelta: 0.001,
restSpeed: 0.01,
...config,
onUpdate: set,
});
/**
* If we're between frames, resync the animation to the frameloop.
*/
if (!indexLegacy.frameData.isProcessing) {
const delta = performance.now() - indexLegacy.frameData.timestamp;
if (delta < 30) {
activeSpringAnimation.current.time =
indexLegacy.millisecondsToSeconds(delta);
}
}
return value.get();
}, stopAnimation);
}, [JSON.stringify(config)]);
useIsomorphicLayoutEffect(() => {
if (indexLegacy.isMotionValue(source)) {
return source.on("change", (v) => value.set(parseFloat(v)));
}
}, [value]);
return value;
}
function useMotionValueEvent(value, event, callback) {
/**
* useInsertionEffect will create subscriptions before any other
* effects will run. Effects run upwards through the tree so it
* can be that binding a useLayoutEffect higher up the tree can
* miss changes from lower down the tree.
*/
React.useInsertionEffect(() => value.on(event, callback), [value, event, callback]);
}
/**
* Creates a `MotionValue` that updates when the velocity of the provided `MotionValue` changes.
*
* ```javascript
* const x = useMotionValue(0)
* const xVelocity = useVelocity(x)
* const xAcceleration = useVelocity(xVelocity)
* ```
*
* @public
*/
function useVelocity(value) {
const velocity = useMotionValue(value.getVelocity());
useMotionValueEvent(value, "velocityChange", (newVelocity) => {
velocity.set(newVelocity);
});
return velocity;
}
function refWarning(name, ref) {
indexLegacy.warning(Boolean(!ref || ref.current), `You have defined a ${name} options but the provided ref is not yet hydrated, probably because it's defined higher up the tree. Try calling useScroll() in the same component as the ref, or setting its \`layoutEffect: false\` option.`);
}
const createScrollMotionValues = () => ({
scrollX: indexLegacy.motionValue(0),
scrollY: indexLegacy.motionValue(0),
scrollXProgress: indexLegacy.motionValue(0),
scrollYProgress: indexLegacy.motionValue(0),
});
function useScroll({ container, target, layoutEffect = true, ...options } = {}) {
const values = useConstant(createScrollMotionValues);
const useLifecycleEffect = layoutEffect
? useIsomorphicLayoutEffect
: React.useEffect;
useLifecycleEffect(() => {
refWarning("target", target);
refWarning("container", container);
return indexLegacy.scrollInfo(({ x, y }) => {
values.scrollX.set(x.current);
values.scrollXProgress.set(x.progress);
values.scrollY.set(y.current);
values.scrollYProgress.set(y.progress);
}, {
...options,
container: (container === null || container === void 0 ? void 0 : container.current) || undefined,
target: (target === null || target === void 0 ? void 0 : target.current) || undefined,
});
}, [container, target, JSON.stringify(options.offset)]);
return values;
}
/**
* @deprecated useElementScroll is deprecated. Convert to useScroll({ container: ref })
*/
function useElementScroll(ref) {
if (process.env.NODE_ENV === "development") {
indexLegacy.warnOnce(false, "useElementScroll is deprecated. Convert to useScroll({ container: ref }).");
}
return useScroll({ container: ref });
}
/**
* @deprecated useViewportScroll is deprecated. Convert to useScroll()
*/
function useViewportScroll() {
if (process.env.NODE_ENV !== "production") {
indexLegacy.warnOnce(false, "useViewportScroll is deprecated. Convert to useScroll().");
}
return useScroll();
}
function useAnimationFrame(callback) {
const initialTimestamp = React.useRef(0);
const { isStatic } = React.useContext(MotionConfigContext);
React.useEffect(() => {
if (isStatic)
return;
const provideTimeSinceStart = ({ timestamp, delta }) => {
if (!initialTimestamp.current)
initialTimestamp.current = timestamp;
callback(timestamp - initialTimestamp.current, delta);
};
indexLegacy.frame.update(provideTimeSinceStart, true);
return () => indexLegacy.cancelFrame(provideTimeSinceStart);
}, [callback]);
}
function useTime() {
const time = useMotionValue(0);
useAnimationFrame((t) => time.set(t));
return time;
}
class WillChangeMotionValue extends indexLegacy.MotionValue {
constructor() {
super(...arguments);
this.members = [];
this.transforms = new Set();
}
add(name) {
let memberName;
if (indexLegacy.transformProps.has(name)) {
this.transforms.add(name);
memberName = "transform";
}
else if (!name.startsWith("origin") &&
!indexLegacy.isCSSVariableName(name) &&
name !== "willChange") {
memberName = indexLegacy.camelToDash(name);
}
if (memberName) {
indexLegacy.addUniqueItem(this.members, memberName);
this.update();
}
}
remove(name) {
if (indexLegacy.transformProps.has(name)) {
this.transforms.delete(name);
if (!this.transforms.size) {
indexLegacy.removeItem(this.members, "transform");
}
}
else {
indexLegacy.removeItem(this.members, indexLegacy.camelToDash(name));
}
this.update();
}
update() {
this.set(this.members.length ? this.members.join(", ") : "auto");
}
}
function useWillChange() {
return useConstant(() => new WillChangeMotionValue("auto"));
}
/**
* A hook that returns `true` if we should be using reduced motion based on the current device's Reduced Motion setting.
*
* This can be used to implement changes to your UI based on Reduced Motion. For instance, replacing motion-sickness inducing
* `x`/`y` animations with `opacity`, disabling the autoplay of background videos, or turning off parallax motion.
*
* It will actively respond to changes and re-render your components with the latest setting.
*
* ```jsx
* export function Sidebar({ isOpen }) {
* const shouldReduceMotion = useReducedMotion()
* const closedX = shouldReduceMotion ? 0 : "-100%"
*
* return (
* <motion.div animate={{
* opacity: isOpen ? 1 : 0,
* x: isOpen ? 0 : closedX
* }} />
* )
* }
* ```
*
* @return boolean
*
* @public
*/
function useReducedMotion() {
/**
* Lazy initialisation of prefersReducedMotion
*/
!indexLegacy.hasReducedMotionListener.current && indexLegacy.initPrefersReducedMotion();
const [shouldReduceMotion] = React.useState(indexLegacy.prefersReducedMotion.current);
if (process.env.NODE_ENV !== "production") {
indexLegacy.warnOnce(shouldReduceMotion !== true, "You have Reduced Motion enabled on your device. Animations may not appear as expected.");
}
/**
* TODO See if people miss automatically updating shouldReduceMotion setting
*/
return shouldReduceMotion;
}
function useReducedMotionConfig() {
const reducedMotionPreference = useReducedMotion();
const { reducedMotion } = React.useContext(MotionConfigContext);
if (reducedMotion === "never") {
return false;
}
else if (reducedMotion === "always") {
return true;
}
else {
return reducedMotionPreference;
}
}
function stopAnimation(visualElement) {
visualElement.values.forEach((value) => value.stop());
}
/**
* @public
*/
function animationControls() {
/**
* Track whether the host component has mounted.
*/
let hasMounted = false;
/**
* A collection of linked component animation controls.
*/
const subscribers = new Set();
const controls = {
subscribe(visualElement) {
subscribers.add(visualElement);
return () => void subscribers.delete(visualElement);
},
start(definition, transitionOverride) {
indexLegacy.invariant(hasMounted, "controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.");
const animations = [];
subscribers.forEach((visualElement) => {
animations.push(animateVisualElement(visualElement, definition, {
transitionOverride,
}));
});
return Promise.all(animations);
},
set(definition) {
indexLegacy.invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
return subscribers.forEach((visualElement) => {
indexLegacy.setValues(visualElement, definition);
});
},
stop() {
subscribers.forEach((visualElement) => {
stopAnimation(visualElement);
});
},
mount() {
hasMounted = true;
return () => {
hasMounted = false;
controls.stop();
};
},
};
return controls;
}
function useAnimate() {
const scope = useConstant(() => ({
current: null,
animations: [],
}));
const animate = useConstant(() => indexLegacy.createScopedAnimate(scope));
useUnmountEffect(() => {
scope.animations.forEach((animation) => animation.stop());
});
return [scope, animate];
}
/**
* Creates `AnimationControls`, which can be used to manually start, stop
* and sequence animations on one or more components.
*
* The returned `AnimationControls` should be passed to the `animate` property
* of the components you want to animate.
*
* These components can then be animated with the `start` method.
*
* ```jsx
* import * as React from 'react'
* import { motion, useAnimation } from 'framer-motion'
*
* export function MyComponent(props) {
* const controls = useAnimation()
*
* controls.start({
* x: 100,
* transition: { duration: 0.5 },
* })
*
* return <motion.div animate={controls} />
* }
* ```
*
* @returns Animation controller with `start` and `stop` methods
*
* @public
*/
function useAnimationControls() {
const controls = useConstant(animationControls);
useIsomorphicLayoutEffect(controls.mount, []);
return controls;
}
const useAnimation = useAnimationControls;
/**
* Cycles through a series of visual properties. Can be used to toggle between or cycle through animations. It works similar to `useState` in React. It is provided an initial array of possible states, and returns an array of two arguments.
*
* An index value can be passed to the returned `cycle` function to cycle to a specific index.
*
* ```jsx
* import * as React from "react"
* import { motion, useCycle } from "framer-motion"
*
* export const MyComponent = () => {
* const [x, cycleX] = useCycle(0, 50, 100)
*
* return (
* <motion.div
* animate={{ x: x }}
* onTap={() => cycleX()}
* />
* )
* }
* ```
*
* @param items - items to cycle through
* @returns [currentState, cycleState]
*
* @public
*/
function useCycle(...items) {
const index = React.useRef(0);
const [item, setItem] = React.useState(items[index.current]);
const runCycle = React.useCallback((next) => {
index.current =
typeof next !== "number"
? indexLegacy.wrap(0, items.length, index.current + 1)
: next;
setItem(items[index.current]);
},
// The array will change on each call, but by putting items.length at
// the front of this array, we guarantee the dependency comparison will match up
// eslint-disable-next-line react-hooks/exhaustive-deps
[items.length, ...items]);
return [item, runCycle];
}
function useInView(ref, { root, margin, amount, once = false } = {}) {
const [isInView, setInView] = React.useState(false);
React.useEffect(() => {
if (!ref.current || (once && isInView))
return;
const onEnter = () => {
setInView(true);
return once ? undefined : () => setInView(false);
};
const options = {
root: (root && root.current) || undefined,
margin,
amount,
};
return indexLegacy.inView(ref.current, onEnter, options);
}, [root, ref, margin, once, amount]);
return isInView;
}
/**
* Can manually trigger a drag gesture on one or more `drag`-enabled `motion` components.
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
class DragControls {
constructor() {
this.componentControls = new Set();
}
/**
* Subscribe a component's internal `VisualElementDragControls` to the user-facing API.
*
* @internal
*/
subscribe(controls) {
this.componentControls.add(controls);
return () => this.componentControls.delete(controls);
}
/**
* Start a drag gesture on every `motion` component that has this set of drag controls
* passed into it via the `dragControls` prop.
*
* ```jsx
* dragControls.start(e, {
* snapToCursor: true
* })
* ```
*
* @param event - PointerEvent
* @param options - Options
*
* @public
*/
start(event, options) {
this.componentControls.forEach((controls) => {
controls.start(event.nativeEvent || event, options);
});
}
}
const createDragControls = () => new DragControls();
/**
* Usually, dragging is initiated by pressing down on a `motion` component with a `drag` prop
* and moving it. For some use-cases, for instance clicking at an arbitrary point on a video scrubber, we
* might want to initiate that dragging from a different component than the draggable one.
*
* By creating a `dragControls` using the `useDragControls` hook, we can pass this into
* the draggable component's `dragControls` prop. It exposes a `start` method
* that can start dragging from pointer events on other components.
*
* ```jsx
* const dragControls = useDragControls()
*
* function startDrag(event) {
* dragControls.start(event, { snapToCursor: true })
* }
*
* return (
* <>
* <div onPointerDown={startDrag} />
* <motion.div drag="x" dragControls={dragControls} />
* </>
* )
* ```
*
* @public
*/
function useDragControls() {
return useConstant(createDragControls);
}
/**
* Attaches an event listener directly to the provided DOM element.
*
* Bypassing React's event system can be desirable, for instance when attaching non-passive
* event handlers.
*
* ```jsx
* const ref = useRef(null)
*
* useDomEvent(ref, 'wheel', onWheel, { passive: false })
*
* return <div ref={ref} />
* ```
*
* @param ref - React.RefObject that's been provided to the element you want to bind the listener to.
* @param eventName - Name of the event you want listen for.
* @param handler - Function to fire when receiving the event.
* @param options - Options to pass to `Event.addEventListener`.
*
* @public
*/
function useDomEvent(ref, eventName, handler, options) {
React.useEffect(() => {
const element = ref.current;
if (handler && element) {
return addDomEvent(element, eventName, handler, options);
}
}, [ref, eventName, handler, options]);
}
/**
* Checks if a component is a `motion` component.
*/
function isMotionComponent(component) {
return (component !== null &&
typeof component === "object" &&
motionComponentSymbol in component);
}
/**
* Unwraps a `motion` component and returns either a string for `motion.div` or
* the React component for `motion(Component)`.
*
* If the component is not a `motion` component it returns undefined.
*/
function unwrapMotionComponent(component) {
if (isMotionComponent(component)) {
return component[motionComponentSymbol];
}
return undefined;
}
function useInstantLayoutTransition() {
return startTransition;
}
function startTransition(callback) {
if (!rootProjectionNode.current)
return;
rootProjectionNode.current.isUpdating = false;
rootProjectionNode.current.blockUpdate();
callback && callback();
}
function useInstantTransition() {
const [forceUpdate, forcedRenderCount] = useForceUpdate();
const startInstantLayoutTransition = useInstantLayoutTransition();
const unlockOnFrameRef = React.useRef();
React.useEffect(() => {
/**
* Unblock after two animation frames, otherwise this will unblock too soon.
*/
indexLegacy.frame.postRender(() => indexLegacy.frame.postRender(() => {
/**
* If the callback has been called again after the effect
* triggered this 2 frame delay, don't unblock animations. This
* prevents the previous effect from unblocking the current
* instant transition too soon. This becomes more likely when
* used in conjunction with React.startTransition().
*/
if (forcedRenderCount !== unlockOnFrameRef.current)
return;
indexLegacy.instantAnimationState.current = false;
}));
}, [forcedRenderCount]);
return (callback) => {
startInstantLayoutTransition(() => {
indexLegacy.instantAnimationState.current = true;
forceUpdate();
callback();
unlockOnFrameRef.current = forcedRenderCount + 1;
});
};
}
function disableInstantTransitions() {
indexLegacy.instantAnimationState.current = false;
}
function useResetProjection() {
const reset = React__namespace.useCallback(() => {
const root = rootProjectionNode.current;
if (!root)
return;
root.resetTree();
}, []);
return reset;
}
const appearStoreId = (id, value) => `${id}: ${value}`;
const appearAnimationStore = new Map();
let handoffFrameTime;
function handoffOptimizedAppearAnimation(elementId, valueName,
/**
* Legacy arguments. This function is inlined as part of SSG so it can be there's
* a version mismatch between the main included Motion and the inlined script.
*
* Remove in early 2024.
*/
_value, _frame) {
const optimisedValueName = indexLegacy.transformProps.has(valueName)
? "transform"
: valueName;
const storeId = appearStoreId(elementId, optimisedValueName);
const optimisedAnimation = appearAnimationStore.get(storeId);
if (!optimisedAnimation) {
return null;
}
const { animation, startTime } = optimisedAnimation;
const cancelAnimation = () => {
appearAnimationStore.delete(storeId);
try {
animation.cancel();
}
catch (error) { }
};
/**
* If the startTime is null, this animation is the Paint Ready detection animation
* and we can cancel it immediately without handoff.
*
* Or if we've already handed off the animation then we're now interrupting it.
* In which case we need to cancel it.
*/
if (startTime === null || window.HandoffComplete) {
cancelAnimation();
return null;
}
else {
/**
* Otherwise we're handing off this animation to the main thread.
*
* Record the time of the first handoff. We call performance.now() once
* here and once in startOptimisedAnimation to ensure we're getting
* close to a frame-locked time. This keeps all animations in sync.
*/
if (handoffFrameTime === undefined) {
handoffFrameTime = performance.now();
}
/**
* We use main thread timings vs those returned by Animation.currentTime as it
* can be the case, particularly in Firefox, that currentTime doesn't return
* an updated value for several frames, even as the animation plays smoothly via
* the GPU.
*/
return handoffFrameTime - startTime || 0;
}
}
/**
* A single time to use across all animations to manually set startTime
* and ensure they're all in sync.
*/
let startFrameTime;
/**
* A dummy animation to detect when Chrome is ready to start
* painting the page and hold off from triggering the real animation
* until then. We only need one animation to detect paint ready.
*
* https://bugs.chromium.org/p/chromium/issues/detail?id=1406850
*/
let readyAnimation;
function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
// Prevent optimised appear animations if Motion has already started animating.
if (window.HandoffComplete) {
window.HandoffAppearAnimations = undefined;
return;
}
const id = element.dataset[indexLegacy.optimizedAppearDataId];
if (!id)
return;
window.HandoffAppearAnimations = handoffOptimizedAppearAnimation;
const storeId = appearStoreId(id, name);
if (!readyAnimation) {
readyAnimation = indexLegacy.animateStyle(element, name, [keyframes[0], keyframes[0]],
/**
* 10 secs is basically just a super-safe duration to give Chrome
* long enough to get the animation ready.
*/
{ duration: 10000, ease: "linear" });
appearAnimationStore.set(storeId, {
animation: readyAnimation,
startTime: null,
});
}
const startAnimation = () => {
readyAnimation.cancel();
const appearAnimation = indexLegacy.animateStyle(element, name, keyframes, options);
/**
* Record the time of the first started animation. We call performance.now() once
* here and once in handoff to ensure we're getting
* close to a frame-locked time. This keeps all animations in sync.
*/
if (startFrameTime === undefined) {
startFrameTime = performance.now();
}
appearAnimation.startTime = startFrameTime;
appearAnimationStore.set(storeId, {
animation: appearAnimation,
startTime: startFrameTime,
});
if (onReady)
onReady(appearAnimation);
};
if (readyAnimation.ready) {
readyAnimation.ready.then(startAnimation).catch(indexLegacy.noop);
}
else {
startAnimation();
}
}
const createObject = () => ({});
class StateVisualElement extends indexLegacy.VisualElement {
build() { }
measureInstanceViewportBox() {
return indexLegacy.createBox();
}
resetTransform() { }
restoreTransform() { }
removeValueFromRenderState() { }
renderInstance() { }
scrapeMotionValuesFromProps() {
return createObject();
}
getBaseTargetFromProps() {
return undefined;
}
readValueFromInstance(_state, key, options) {
return options.initialState[key] || 0;
}
sortInstanceNodePosition() {
return 0;
}
makeTargetAnimatableFromInstance({ transition, transitionEnd, ...target }) {
const origin = indexLegacy.getOrigin(target, transition || {}, this);
indexLegacy.checkTargetForNewValues(this, target, origin);
return { transition, transitionEnd, ...target };
}
}
const useVisualState = makeUseVisualState({
scrapeMotionValuesFromProps: createObject,
createRenderState: createObject,
});
/**
* This is not an officially supported API and may be removed
* on any version.
*/
function useAnimatedState(initialState) {
const [animationState, setAnimationState] = React.useState(initialState);
const visualState = useVisualState({}, false);
const element = useConstant(() => {
return new StateVisualElement({ props: {}, visualState, presenceContext: null }, { initialState });
});
React.useEffect(() => {
element.mount({});
return () => element.unmount();
}, [element]);
React.useEffect(() => {
element.update({
onUpdate: (v) => {
setAnimationState({ ...v });
},
}, null);
}, [setAnimationState, element]);
const startAnimation = useConstant(() => (animationDefinition) => {
return animateVisualElement(element, animationDefinition);
});
return [animationState, startAnimation];
}
// Keep things reasonable and avoid scale: Infinity. In practise we might need
// to add another value, opacity, that could interpolate scaleX/Y [0,0.01] => [0,1]
// to simply hide content at unreasonable scales.
const maxScale = 100000;
const invertScale = (scale) => scale > 0.001 ? 1 / scale : maxScale;
let hasWarned = false;
/**
* Returns a `MotionValue` each for `scaleX` and `scaleY` that update with the inverse
* of their respective parent scales.
*
* This is useful for undoing the distortion of content when scaling a parent component.
*
* By default, `useInvertedScale` will automatically fetch `scaleX` and `scaleY` from the nearest parent.
* By passing other `MotionValue`s in as `useInvertedScale({ scaleX, scaleY })`, it will invert the output
* of those instead.
*
* ```jsx
* const MyComponent = () => {
* const { scaleX, scaleY } = useInvertedScale()
* return <motion.div style={{ scaleX, scaleY }} />
* }
* ```
*
* @deprecated
*/
function useInvertedScale(scale) {
let parentScaleX = useMotionValue(1);
let parentScaleY = useMotionValue(1);
const { visualElement } = React.useContext(MotionContext);
indexLegacy.invariant(!!(scale || visualElement), "If no scale values are provided, useInvertedScale must be used within a child of another motion component.");
indexLegacy.warning(hasWarned, "useInvertedScale is deprecated and will be removed in 3.0. Use the layout prop instead.");
hasWarned = true;
if (scale) {
parentScaleX = scale.scaleX || parentScaleX;
parentScaleY = scale.scaleY || parentScaleY;
}
else if (visualElement) {
parentScaleX = visualElement.getValue("scaleX", 1);
parentScaleY = visualElement.getValue("scaleY", 1);
}
const scaleX = useTransform(parentScaleX, invertScale);
const scaleY = useTransform(parentScaleY, invertScale);
return { scaleX, scaleY };
}
let id = 0;
const AnimateSharedLayout = ({ children }) => {
React__namespace.useEffect(() => {
indexLegacy.invariant(false, "AnimateSharedLayout is deprecated: https://www.framer.com/docs/guide-upgrade/##shared-layout-animations");
}, []);
return (React__namespace.createElement(LayoutGroup, { id: useConstant(() => `asl-${id++}`) }, children));
};
exports.MotionGlobalConfig = indexLegacy.MotionGlobalConfig;
exports.MotionValue = indexLegacy.MotionValue;
exports.VisualElement = indexLegacy.VisualElement;
exports.addScaleCorrector = indexLegacy.addScaleCorrector;
exports.animate = indexLegacy.animate;
exports.animateValue = indexLegacy.animateValue;
exports.anticipate = indexLegacy.anticipate;
exports.backIn = indexLegacy.backIn;
exports.backInOut = indexLegacy.backInOut;
exports.backOut = indexLegacy.backOut;
exports.buildTransform = indexLegacy.buildTransform;
exports.cancelFrame = indexLegacy.cancelFrame;
exports.cancelSync = indexLegacy.cancelSync;
exports.checkTargetForNewValues = indexLegacy.checkTargetForNewValues;
exports.circIn = indexLegacy.circIn;
exports.circInOut = indexLegacy.circInOut;
exports.circOut = indexLegacy.circOut;
exports.clamp = indexLegacy.clamp;
exports.color = indexLegacy.color;
exports.complex = indexLegacy.complex;
exports.createBox = indexLegacy.createBox;
exports.createScopedAnimate = indexLegacy.createScopedAnimate;
exports.cubicBezier = indexLegacy.cubicBezier;
exports.delay = indexLegacy.delay;
exports.distance = indexLegacy.distance;
exports.distance2D = indexLegacy.distance2D;
exports.easeIn = indexLegacy.easeIn;
exports.easeInOut = indexLegacy.easeInOut;
exports.easeOut = indexLegacy.easeOut;
exports.frame = indexLegacy.frame;
exports.frameData = indexLegacy.frameData;
exports.inView = indexLegacy.inView;
exports.interpolate = indexLegacy.interpolate;
Object.defineProperty(exports, 'invariant', {
enumerable: true,
get: function () { return indexLegacy.invariant; }
});
exports.isBrowser = indexLegacy.isBrowser;
exports.isMotionValue = indexLegacy.isMotionValue;
exports.mirrorEasing = indexLegacy.mirrorEasing;
exports.mix = indexLegacy.mix;
exports.motionValue = indexLegacy.motionValue;
exports.optimizedAppearDataAttribute = indexLegacy.optimizedAppearDataAttribute;
exports.pipe = indexLegacy.pipe;
exports.progress = indexLegacy.progress;
exports.px = indexLegacy.px;
exports.reverseEasing = indexLegacy.reverseEasing;
exports.scroll = indexLegacy.scroll;
exports.scrollInfo = indexLegacy.scrollInfo;
exports.spring = indexLegacy.spring;
exports.stagger = indexLegacy.stagger;
exports.steps = indexLegacy.steps;
exports.sync = indexLegacy.sync;
exports.transform = indexLegacy.transform;
exports.visualElementStore = indexLegacy.visualElementStore;
Object.defineProperty(exports, 'warning', {
enumerable: true,
get: function () { return indexLegacy.warning; }
});
exports.wrap = indexLegacy.wrap;
exports.AnimatePresence = AnimatePresence;
exports.AnimateSharedLayout = AnimateSharedLayout;
exports.DeprecatedLayoutGroupContext = DeprecatedLayoutGroupContext;
exports.DragControls = DragControls;
exports.FlatTree = FlatTree;
exports.LayoutGroup = LayoutGroup;
exports.LayoutGroupContext = LayoutGroupContext;
exports.LazyMotion = LazyMotion;
exports.MotionConfig = MotionConfig;
exports.MotionConfigContext = MotionConfigContext;
exports.MotionContext = MotionContext;
exports.PresenceContext = PresenceContext;
exports.Reorder = Reorder;
exports.SwitchLayoutGroupContext = SwitchLayoutGroupContext;
exports.addPointerEvent = addPointerEvent;
exports.addPointerInfo = addPointerInfo;
exports.animateVisualElement = animateVisualElement;
exports.animationControls = animationControls;
exports.animations = animations;
exports.calcLength = calcLength;
exports.createDomMotionComponent = createDomMotionComponent;
exports.createMotionComponent = createMotionComponent;
exports.disableInstantTransitions = disableInstantTransitions;
exports.domAnimation = domAnimation;
exports.domMax = domMax;
exports.filterProps = filterProps;
exports.isDragActive = isDragActive;
exports.isMotionComponent = isMotionComponent;
exports.isValidMotionProp = isValidMotionProp;
exports.m = m;
exports.makeUseVisualState = makeUseVisualState;
exports.motion = motion;
exports.resolveMotionValue = resolveMotionValue;
exports.startOptimizedAppearAnimation = startOptimizedAppearAnimation;
exports.unwrapMotionComponent = unwrapMotionComponent;
exports.useAnimate = useAnimate;
exports.useAnimation = useAnimation;
exports.useAnimationControls = useAnimationControls;
exports.useAnimationFrame = useAnimationFrame;
exports.useCycle = useCycle;
exports.useDeprecatedAnimatedState = useAnimatedState;
exports.useDeprecatedInvertedScale = useInvertedScale;
exports.useDomEvent = useDomEvent;
exports.useDragControls = useDragControls;
exports.useElementScroll = useElementScroll;
exports.useForceUpdate = useForceUpdate;
exports.useInView = useInView;
exports.useInstantLayoutTransition = useInstantLayoutTransition;
exports.useInstantTransition = useInstantTransition;
exports.useIsPresent = useIsPresent;
exports.useIsomorphicLayoutEffect = useIsomorphicLayoutEffect;
exports.useMotionTemplate = useMotionTemplate;
exports.useMotionValue = useMotionValue;
exports.useMotionValueEvent = useMotionValueEvent;
exports.usePresence = usePresence;
exports.useReducedMotion = useReducedMotion;
exports.useReducedMotionConfig = useReducedMotionConfig;
exports.useResetProjection = useResetProjection;
exports.useScroll = useScroll;
exports.useSpring = useSpring;
exports.useTime = useTime;
exports.useTransform = useTransform;
exports.useUnmountEffect = useUnmountEffect;
exports.useVelocity = useVelocity;
exports.useViewportScroll = useViewportScroll;
exports.useWillChange = useWillChange;