framer-motion
Version:
A simple and powerful React animation library
1,368 lines (1,296 loc) • 417 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) :
typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Motion = {}, global.React));
})(this, (function (exports, React) { 'use strict';
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__default = /*#__PURE__*/_interopDefaultLegacy(React);
var React__namespace = /*#__PURE__*/_interopNamespace(React);
/**
* @public
*/
const MotionConfigContext = React.createContext({
transformPagePoint: (p) => p,
isStatic: false,
reducedMotion: "never",
});
const MotionContext = React.createContext({});
function useVisualElementContext() {
return React.useContext(MotionContext).visualElement;
}
/**
* @public
*/
const PresenceContext = React.createContext(null);
const isBrowser = typeof document !== "undefined";
const useIsomorphicLayoutEffect = isBrowser ? React.useLayoutEffect : React.useEffect;
const LazyContext = React.createContext({ strict: false });
function useVisualElement(Component, visualState, props, createVisualElement) {
const parent = useVisualElementContext();
const lazyContext = React.useContext(LazyContext);
const presenceContext = React.useContext(PresenceContext);
const reducedMotionConfig = React.useContext(MotionConfigContext).reducedMotion;
const visualElementRef = React.useRef(undefined);
/**
* 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,
presenceId: presenceContext ? presenceContext.id : undefined,
blockInitialAnimation: presenceContext
? presenceContext.initial === false
: false,
reducedMotionConfig,
});
}
const visualElement = visualElementRef.current;
useIsomorphicLayoutEffect(() => {
visualElement && visualElement.syncRender();
});
React.useEffect(() => {
if (visualElement && visualElement.animationState) {
visualElement.animationState.animateChanges();
}
});
useIsomorphicLayoutEffect(() => () => visualElement && visualElement.notifyUnmount(), []);
return visualElement;
}
function isRefObject(ref) {
return (typeof ref === "object" &&
Object.prototype.hasOwnProperty.call(ref, "current"));
}
/**
* 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 (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]);
}
/**
* Decides if the supplied variable is variant label
*/
function isVariantLabel(v) {
return typeof v === "string" || Array.isArray(v);
}
function isAnimationControls(v) {
return typeof v === "object" && typeof v.start === "function";
}
const variantProps$1 = [
"initial",
"animate",
"exit",
"whileHover",
"whileDrag",
"whileTap",
"whileFocus",
"whileInView",
];
function isControllingVariants(props) {
return (isAnimationControls(props.animate) ||
variantProps$1.some((name) => isVariantLabel(props[name])));
}
function isVariantNode(props) {
return Boolean(isControllingVariants(props) || props.variants);
}
function getCurrentTreeVariants(props, context) {
if (isControllingVariants(props)) {
const { initial, animate } = props;
return {
initial: initial === false || isVariantLabel(initial)
? initial
: undefined,
animate: 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;
}
const createDefinition = (propNames) => ({
isEnabled: (props) => propNames.some((name) => !!props[name]),
});
const featureDefinitions = {
measureLayout: createDefinition(["layout", "layoutId", "drag"]),
animation: createDefinition([
"animate",
"exit",
"variants",
"whileHover",
"whileTap",
"whileFocus",
"whileDrag",
"whileInView",
]),
exit: createDefinition(["exit"]),
drag: createDefinition(["drag", "dragControls"]),
focus: createDefinition(["whileFocus"]),
hover: createDefinition(["whileHover", "onHoverStart", "onHoverEnd"]),
tap: createDefinition(["whileTap", "onTap", "onTapStart", "onTapCancel"]),
pan: createDefinition([
"onPan",
"onPanStart",
"onPanSessionStart",
"onPanEnd",
]),
inView: createDefinition([
"whileInView",
"onViewportEnter",
"onViewportLeave",
]),
};
function loadFeatures(features) {
for (const key in features) {
if (key === "projectionNodeConstructor") {
featureDefinitions.projectionNodeConstructor = features[key];
}
else {
featureDefinitions[key].Component = features[key];
}
}
}
/**
* 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;
}
/**
* 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,
};
let id$1 = 1;
function useProjectionId() {
return useConstant(() => {
if (globalProjectionState.hasEverUpdated) {
return id$1++;
}
});
}
const LayoutGroupContext = React.createContext({});
class VisualElementHandler extends React__default["default"].Component {
/**
* Update visual element props as soon as we know this update is going to be commited.
*/
getSnapshotBeforeUpdate() {
const { visualElement, props } = this.props;
if (visualElement)
visualElement.setProps(props);
return null;
}
componentDidUpdate() { }
render() {
return this.props.children;
}
}
/**
* 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, projectionNodeConstructor, useRender, useVisualState, Component, }) {
preloadedFeatures && loadFeatures(preloadedFeatures);
function MotionComponent(props, externalRef) {
const configAndProps = {
...React.useContext(MotionConfigContext),
...props,
layoutId: useLayoutId(props),
};
const { isStatic } = configAndProps;
let features = null;
const context = useCreateMotionContext(props);
/**
* Create a unique projection ID for this component. If a new component is added
* during a layout animation we'll use this to query the DOM and hydrate its ref early, allowing
* us to measure it as soon as any layout effect flushes pending layout animations.
*
* Performance note: It'd be better not to have to search the DOM for these elements.
* For newly-entering components it could be enough to only correct treeScale, in which
* case we could mount in a scale-correction mode. This wouldn't be enough for
* shared element transitions however. Perhaps for those we could revert to a root node
* that gets forceRendered and layout animations are triggered on its layout effect.
*/
const projectionId = isStatic ? undefined : useProjectionId();
/**
*
*/
const visualState = useVisualState(props, isStatic);
if (!isStatic && 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 lazyStrictMode = React.useContext(LazyContext).strict;
const initialLayoutGroupConfig = React.useContext(SwitchLayoutGroupContext);
if (context.visualElement) {
features = context.visualElement.loadFeatures(props, lazyStrictMode, preloadedFeatures, projectionId, projectionNodeConstructor ||
featureDefinitions.projectionNodeConstructor, 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(VisualElementHandler, { visualElement: context.visualElement, props: configAndProps },
features,
React__namespace.createElement(MotionContext.Provider, { value: context }, useRender(Component, props, projectionId, 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",
"svg",
"switch",
"symbol",
"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 scaleCorrectors = {};
function addScaleCorrector(correctors) {
Object.assign(scaleCorrectors, correctors);
}
/**
* Generate a list of every possible transform key.
*/
const transformPropOrder = [
"transformPerspective",
"x",
"y",
"z",
"translateX",
"translateY",
"translateZ",
"scale",
"scaleX",
"scaleY",
"rotate",
"rotateX",
"rotateY",
"rotateZ",
"skew",
"skewX",
"skewY",
];
/**
* A quick lookup for transform props.
*/
const transformProps = new Set(transformPropOrder);
function isForcedMotionValue(key, { layout, layoutId }) {
return (transformProps.has(key) ||
key.startsWith("origin") ||
((layout || layoutId !== undefined) &&
(!!scaleCorrectors[key] || key === "opacity")));
}
const isMotionValue = (value) => value === undefined ? false : !!value.getVelocity;
const translateAlias = {
x: "translateX",
y: "translateY",
z: "translateZ",
transformPerspective: "perspective",
};
/**
* A function to use with Array.sort to sort transform keys by their default order.
*/
const sortTransformProps = (a, b) => transformPropOrder.indexOf(a) - transformPropOrder.indexOf(b);
/**
* Build a CSS transform style from individual x/y/scale etc properties.
*
* This outputs with a default order of transforms/scales/rotations, this can be customised by
* providing a transformTemplate function.
*/
function buildTransform({ transform, transformKeys }, { enableHardwareAcceleration = true, allowTransformNone = true, }, transformIsDefault, transformTemplate) {
// The transform string we're going to build into.
let transformString = "";
// Transform keys into their default order - this will determine the output order.
transformKeys.sort(sortTransformProps);
// Loop over each transform and build them into transformString
for (const key of transformKeys) {
transformString += `${translateAlias[key] || key}(${transform[key]}) `;
}
if (enableHardwareAcceleration && !transform.z) {
transformString += "translateZ(0)";
}
transformString = transformString.trim();
// If we have a custom `transform` template, pass our transform values and
// generated transformString to that before returning
if (transformTemplate) {
transformString = transformTemplate(transform, transformIsDefault ? "" : transformString);
}
else if (allowTransformNone && transformIsDefault) {
transformString = "none";
}
return transformString;
}
/**
* Returns true if the provided key is a CSS variable
*/
function isCSSVariable$1(key) {
return key.startsWith("--");
}
/**
* Provided a value and a ValueType, returns the value as that value type.
*/
const getValueAsType = (value, type) => {
return type && typeof value === "number"
? type.transform(value)
: value;
};
const clamp$2 = (min, max) => (v) => Math.max(Math.min(v, max), min);
const sanitize = (v) => (v % 1 ? Number(v.toFixed(5)) : v);
const floatRegex = /(-)?([\d]*\.?[\d])+/g;
const colorRegex = /(#[0-9a-f]{6}|#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))/gi;
const singleColorRegex = /^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?[\d\.]+%?[,\s]+){2}(-?[\d\.]+%?)\s*[\,\/]?\s*[\d\.]*%?\))$/i;
function isString$1(v) {
return typeof v === 'string';
}
const number = {
test: (v) => typeof v === 'number',
parse: parseFloat,
transform: (v) => v,
};
const alpha = Object.assign(Object.assign({}, number), { transform: clamp$2(0, 1) });
const scale = Object.assign(Object.assign({}, number), { default: 1 });
const createUnitType = (unit) => ({
test: (v) => isString$1(v) && v.endsWith(unit) && v.split(' ').length === 1,
parse: parseFloat,
transform: (v) => `${v}${unit}`,
});
const degrees = createUnitType('deg');
const percent = createUnitType('%');
const px = createUnitType('px');
const vh = createUnitType('vh');
const vw = createUnitType('vw');
const progressPercentage = Object.assign(Object.assign({}, percent), { parse: (v) => percent.parse(v) / 100, transform: (v) => percent.transform(v * 100) });
const isColorString = (type, testProp) => (v) => {
return Boolean((isString$1(v) && singleColorRegex.test(v) && v.startsWith(type)) ||
(testProp && Object.prototype.hasOwnProperty.call(v, testProp)));
};
const splitColor = (aName, bName, cName) => (v) => {
if (!isString$1(v))
return v;
const [a, b, c, alpha] = v.match(floatRegex);
return {
[aName]: parseFloat(a),
[bName]: parseFloat(b),
[cName]: parseFloat(c),
alpha: alpha !== undefined ? parseFloat(alpha) : 1,
};
};
const hsla = {
test: isColorString('hsl', 'hue'),
parse: splitColor('hue', 'saturation', 'lightness'),
transform: ({ hue, saturation, lightness, alpha: alpha$1 = 1 }) => {
return ('hsla(' +
Math.round(hue) +
', ' +
percent.transform(sanitize(saturation)) +
', ' +
percent.transform(sanitize(lightness)) +
', ' +
sanitize(alpha.transform(alpha$1)) +
')');
},
};
const clampRgbUnit = clamp$2(0, 255);
const rgbUnit = Object.assign(Object.assign({}, number), { transform: (v) => Math.round(clampRgbUnit(v)) });
const rgba = {
test: isColorString('rgb', 'red'),
parse: splitColor('red', 'green', 'blue'),
transform: ({ red, green, blue, alpha: alpha$1 = 1 }) => 'rgba(' +
rgbUnit.transform(red) +
', ' +
rgbUnit.transform(green) +
', ' +
rgbUnit.transform(blue) +
', ' +
sanitize(alpha.transform(alpha$1)) +
')',
};
function parseHex(v) {
let r = '';
let g = '';
let b = '';
let a = '';
if (v.length > 5) {
r = v.substr(1, 2);
g = v.substr(3, 2);
b = v.substr(5, 2);
a = v.substr(7, 2);
}
else {
r = v.substr(1, 1);
g = v.substr(2, 1);
b = v.substr(3, 1);
a = v.substr(4, 1);
r += r;
g += g;
b += b;
a += a;
}
return {
red: parseInt(r, 16),
green: parseInt(g, 16),
blue: parseInt(b, 16),
alpha: a ? parseInt(a, 16) / 255 : 1,
};
}
const hex = {
test: isColorString('#'),
parse: parseHex,
transform: rgba.transform,
};
const color = {
test: (v) => rgba.test(v) || hex.test(v) || hsla.test(v),
parse: (v) => {
if (rgba.test(v)) {
return rgba.parse(v);
}
else if (hsla.test(v)) {
return hsla.parse(v);
}
else {
return hex.parse(v);
}
},
transform: (v) => {
return isString$1(v)
? v
: v.hasOwnProperty('red')
? rgba.transform(v)
: hsla.transform(v);
},
};
const colorToken = '${c}';
const numberToken = '${n}';
function test(v) {
var _a, _b, _c, _d;
return (isNaN(v) &&
isString$1(v) &&
((_b = (_a = v.match(floatRegex)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) + ((_d = (_c = v.match(colorRegex)) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0);
}
function analyse$1(v) {
if (typeof v === 'number')
v = `${v}`;
const values = [];
let numColors = 0;
const colors = v.match(colorRegex);
if (colors) {
numColors = colors.length;
v = v.replace(colorRegex, colorToken);
values.push(...colors.map(color.parse));
}
const numbers = v.match(floatRegex);
if (numbers) {
v = v.replace(floatRegex, numberToken);
values.push(...numbers.map(number.parse));
}
return { values, numColors, tokenised: v };
}
function parse(v) {
return analyse$1(v).values;
}
function createTransformer(v) {
const { values, numColors, tokenised } = analyse$1(v);
const numValues = values.length;
return (v) => {
let output = tokenised;
for (let i = 0; i < numValues; i++) {
output = output.replace(i < numColors ? colorToken : numberToken, i < numColors ? color.transform(v[i]) : sanitize(v[i]));
}
return output;
};
}
const convertNumbersToZero = (v) => typeof v === 'number' ? 0 : v;
function getAnimatableNone$1(v) {
const parsed = parse(v);
const transformer = createTransformer(v);
return transformer(parsed.map(convertNumbersToZero));
}
const complex = { test, parse, createTransformer, getAnimatableNone: getAnimatableNone$1 };
const maxDefaults = new Set(['brightness', 'contrast', 'saturate', 'opacity']);
function applyDefaultFilter(v) {
let [name, value] = v.slice(0, -1).split('(');
if (name === 'drop-shadow')
return v;
const [number] = value.match(floatRegex) || [];
if (!number)
return v;
const unit = value.replace(number, '');
let defaultValue = maxDefaults.has(name) ? 1 : 0;
if (number !== value)
defaultValue *= 100;
return name + '(' + defaultValue + unit + ')';
}
const functionRegex = /([a-z-]*)\(.*?\)/g;
const filter = Object.assign(Object.assign({}, complex), { getAnimatableNone: (v) => {
const functions = v.match(functionRegex);
return functions ? functions.map(applyDefaultFilter).join(' ') : v;
} });
const int = {
...number,
transform: Math.round,
};
const numberValueTypes = {
// Border props
borderWidth: px,
borderTopWidth: px,
borderRightWidth: px,
borderBottomWidth: px,
borderLeftWidth: px,
borderRadius: px,
radius: px,
borderTopLeftRadius: px,
borderTopRightRadius: px,
borderBottomRightRadius: px,
borderBottomLeftRadius: px,
// Positioning props
width: px,
maxWidth: px,
height: px,
maxHeight: px,
size: px,
top: px,
right: px,
bottom: px,
left: px,
// Spacing props
padding: px,
paddingTop: px,
paddingRight: px,
paddingBottom: px,
paddingLeft: px,
margin: px,
marginTop: px,
marginRight: px,
marginBottom: px,
marginLeft: px,
// Transform props
rotate: degrees,
rotateX: degrees,
rotateY: degrees,
rotateZ: degrees,
scale,
scaleX: scale,
scaleY: scale,
scaleZ: scale,
skew: degrees,
skewX: degrees,
skewY: degrees,
distance: px,
translateX: px,
translateY: px,
translateZ: px,
x: px,
y: px,
z: px,
perspective: px,
transformPerspective: px,
opacity: alpha,
originX: progressPercentage,
originY: progressPercentage,
originZ: px,
// Misc
zIndex: int,
// SVG
fillOpacity: alpha,
strokeOpacity: alpha,
numOctaves: int,
};
function buildHTMLStyles(state, latestValues, options, transformTemplate) {
const { style, vars, transform, transformKeys, transformOrigin } = state;
transformKeys.length = 0;
// Track whether we encounter any transform or transformOrigin values.
let hasTransform = false;
let hasTransformOrigin = false;
// Does the calculated transform essentially equal "none"?
let transformIsNone = true;
/**
* Loop over all our latest animated values and decide whether to handle them
* as a style or CSS variable.
*
* Transforms and transform origins are kept seperately for further processing.
*/
for (const key in latestValues) {
const value = latestValues[key];
/**
* If this is a CSS variable we don't do any further processing.
*/
if (isCSSVariable$1(key)) {
vars[key] = value;
continue;
}
// Convert the value to its default value type, ie 0 -> "0px"
const valueType = numberValueTypes[key];
const valueAsType = getValueAsType(value, valueType);
if (transformProps.has(key)) {
// If this is a transform, flag to enable further transform processing
hasTransform = true;
transform[key] = valueAsType;
transformKeys.push(key);
// If we already know we have a non-default transform, early return
if (!transformIsNone)
continue;
// Otherwise check to see if this is a default transform
if (value !== (valueType.default || 0))
transformIsNone = false;
}
else if (key.startsWith("origin")) {
// If this is a transform origin, flag and enable further transform-origin processing
hasTransformOrigin = true;
transformOrigin[key] = valueAsType;
}
else {
style[key] = valueAsType;
}
}
if (hasTransform || transformTemplate) {
style.transform = buildTransform(state, options, transformIsNone, transformTemplate);
}
else if (!latestValues.transform && style.transform) {
/**
* If we have previously created a transform but currently don't have any,
* reset transform style to none.
*/
style.transform = "none";
}
/**
* Build a transformOrigin style. Uses the same defaults as the browser for
* undefined origins.
*/
if (hasTransformOrigin) {
const { originX = "50%", originY = "50%", originZ = 0, } = transformOrigin;
style.transformOrigin = `${originX} ${originY} ${originZ}`;
}
}
const createHtmlRenderState = () => ({
style: {},
transform: {},
transformKeys: [],
transformOrigin: {},
vars: {},
});
function copyRawValuesOnly(target, source, props) {
for (const key in source) {
if (!isMotionValue(source[key]) && !isForcedMotionValue(key, props)) {
target[key] = source[key];
}
}
}
function useInitialMotionValues({ transformTemplate }, visualState, isStatic) {
return React.useMemo(() => {
const state = createHtmlRenderState();
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"}`;
}
htmlProps.style = style;
return htmlProps;
}
const animationProps = [
"animate",
"exit",
"variants",
"whileHover",
"whileTap",
"whileFocus",
"whileDrag",
"whileInView",
];
const tapProps = ["whileTap", "onTap", "onTapStart", "onTapCancel"];
const panProps = ["onPan", "onPanStart", "onPanSessionStart", "onPanEnd"];
const inViewProps = [
"whileInView",
"onViewportEnter",
"onViewportLeave",
"viewport",
];
/**
* A list of all valid MotionProps.
*
* @privateRemarks
* This doesn't throw if a `MotionProp` name is missing - it should.
*/
const validMotionProps = new Set([
"initial",
"style",
"variants",
"transition",
"transformTemplate",
"transformValues",
"custom",
"inherit",
"layout",
"layoutId",
"layoutDependency",
"onLayoutAnimationStart",
"onLayoutAnimationComplete",
"onLayoutMeasure",
"onBeforeLayoutMeasure",
"onAnimationStart",
"onAnimationComplete",
"onUpdate",
"onDragStart",
"onDrag",
"onDragEnd",
"onMeasureDragConstraints",
"onDirectionLock",
"onDragTransitionEnd",
"drag",
"dragControls",
"dragListener",
"dragConstraints",
"dragDirectionLock",
"dragSnapToOrigin",
"_dragX",
"_dragY",
"dragElastic",
"dragMomentum",
"dragPropagation",
"dragTransition",
"onHoverStart",
"onHoverEnd",
"layoutScroll",
...inViewProps,
...tapProps,
...animationProps,
...panProps,
]);
/**
* 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 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) {
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;
}
function calcOrigin$1(origin, offset, size) {
return typeof origin === "string"
? origin
: px.transform(offset + size * origin);
}
/**
* The SVG transform origin defaults are different to CSS and is less intuitive,
* so we use the measured dimensions of the SVG to reconcile these.
*/
function calcSVGTransformOrigin(dimensions, originX, originY) {
const pxOriginX = calcOrigin$1(originX, dimensions.x, dimensions.width);
const pxOriginY = calcOrigin$1(originY, dimensions.y, dimensions.height);
return `${pxOriginX} ${pxOriginY}`;
}
const dashKeys = {
offset: "stroke-dashoffset",
array: "stroke-dasharray",
};
const camelKeys = {
offset: "strokeDashoffset",
array: "strokeDasharray",
};
/**
* Build SVG path properties. Uses the path's measured length to convert
* our custom pathLength, pathSpacing and pathOffset into stroke-dashoffset
* and stroke-dasharray attributes.
*
* This function is mutative to reduce per-frame GC.
*/
function buildSVGPath(attrs, length, spacing = 1, offset = 0, useDashCase = true) {
// Normalise path length by setting SVG attribute pathLength to 1
attrs.pathLength = 1;
// We use dash case when setting attributes directly to the DOM node and camel case
// when defining props on a React component.
const keys = useDashCase ? dashKeys : camelKeys;
// Build the dash offset
attrs[keys.offset] = px.transform(-offset);
// Build the dash array
const pathLength = px.transform(length);
const pathSpacing = px.transform(spacing);
attrs[keys.array] = `${pathLength} ${pathSpacing}`;
}
/**
* Build SVG visual attrbutes, like cx and style.transform
*/
function buildSVGAttrs(state, { attrX, attrY, originX, originY, pathLength, pathSpacing = 1, pathOffset = 0,
// This is object creation, which we try to avoid per-frame.
...latest }, options, transformTemplate) {
buildHTMLStyles(state, latest, options, transformTemplate);
state.attrs = state.style;
state.style = {};
const { attrs, style, dimensions } = state;
/**
* However, we apply transforms as CSS transforms. So if we detect a transform we take it from attrs
* and copy it into style.
*/
if (attrs.transform) {
if (dimensions)
style.transform = attrs.transform;
delete attrs.transform;
}
// Parse transformOrigin
if (dimensions &&
(originX !== undefined || originY !== undefined || style.transform)) {
style.transformOrigin = calcSVGTransformOrigin(dimensions, originX !== undefined ? originX : 0.5, originY !== undefined ? originY : 0.5);
}
// Treat x/y not as shortcuts but as actual attributes
if (attrX !== undefined)
attrs.x = attrX;
if (attrY !== undefined)
attrs.y = attrY;
// Build SVG path if one has been defined
if (pathLength !== undefined) {
buildSVGPath(attrs, pathLength, pathSpacing, pathOffset, false);
}
}
const createSvgRenderState = () => ({
...createHtmlRenderState(),
attrs: {},
});
function useSVGProps(props, visualState) {
const visualProps = React.useMemo(() => {
const state = createSvgRenderState();
buildSVGAttrs(state, visualState, { enableHardwareAcceleration: false }, 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, projectionId, ref, { latestValues }, isStatic) => {
const useVisualProps = isSVGComponent(Component)
? useSVGProps
: useHTMLProps;
const visualProps = useVisualProps(props, latestValues, isStatic);
const filteredProps = filterProps(props, typeof Component === "string", forwardMotionProps);
const elementProps = {
...filteredProps,
...visualProps,
ref,
};
if (projectionId) {
elementProps["data-projection-id"] = projectionId;
}
return React.createElement(Component, elementProps);
};
return useRender;
}
/**
* Convert camelCase to dash-case properties.
*/
const camelToDash = (str) => str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
function renderHTML(element, { style, vars }, styleProp, projection) {
Object.assign(element.style, style, projection && projection.getProjectionStyles(styleProp));
// Loop over any CSS variables and assign those.
for (const key in vars) {
element.style.setProperty(key, vars[key]);
}
}
/**
* A set of attribute names that are always read/written as camel case.
*/
const camelCaseAttributes = new Set([
"baseFrequency",
"diffuseConstant",
"kernelMatrix",
"kernelUnitLength",
"keySplines",
"keyTimes",
"limitingConeAngle",
"markerHeight",
"markerWidth",
"numOctaves",
"targetX",
"targetY",
"surfaceScale",
"specularConstant",
"specularExponent",
"stdDeviation",
"tableValues",
"viewBox",
"gradientTransform",
"pathLength",
]);
function renderSVG(element, renderState, _styleProp, projection) {
renderHTML(element, renderState, undefined, projection);
for (const key in renderState.attrs) {
element.setAttribute(!camelCaseAttributes.has(key) ? camelToDash(key) : key, renderState.attrs[key]);
}
}
function scrapeMotionValuesFromProps$1(props) {
const { style } = props;
const newValues = {};
for (const key in style) {
if (isMotionValue(style[key]) || isForcedMotionValue(key, props)) {
newValues[key] = style[key];
}
}
return newValues;
}
function scrapeMotionValuesFromProps(props) {
const newValues = scrapeMotionValuesFromProps$1(props);
for (const key in props) {
if (isMotionValue(props[key])) {
const targetKey = key === "x" || key === "y" ? "attr" + key.toUpperCase() : key;
newValues[targetKey] = props[key];
}
}
return newValues;
}
function resolveVariantFromProps(props, definition, custom, currentValues = {}, currentVelocity = {}) {
/**
* If the variant definition is a function, resolve.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
/**
* If the variant definition is a variant label, or
* the function returned a variant label, resolve.
*/
if (typeof definition === "string") {
definition = props.variants && props.variants[definition];
}
/**
* At this point we've resolved both functions and variant labels,
* but the resolved variant label might itself have been a function.
* If so, resolve. This can only have returned a valid target object.
*/
if (typeof definition === "function") {
definition = definition(custom !== undefined ? custom : props.custom, currentValues, currentVelocity);
}
return definition;
}
const isKeyframesTarget = (v) => {
return Array.isArray(v);
};
const isCustomValue = (v) => {
return Boolean(v && typeof v === "object" && v.mix && v.toValue);
};
const resolveFinalValueInKeyframes = (v) => {
// TODO maybe throw if v.length - 1 is placeholder token?
return isKeyframesTarget(v) ? v[v.length - 1] || 0 : v;
};
/**
* 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 = isMotionValue(value) ? value.get() : value;
return 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$1 = isControllingVariants(props);
const isVariantNode$1 = isVariantNode(props);
if (context &&
isVariantNode$1 &&
!isControllingVariants$1 &&
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" &&
!isAnimationControls(variantToSet)) {
const list = Array.isArray(variantToSet) ? variantToSet : [variantToSet];
list.forEach((definition) => {
const resolved = resolveVariantFromProps(props, definition);
if (!resolved)
return;
const { transitionEnd, transition, ...target } = resolved;
for (const key in target) {
let valueTarget = target[key];
if (Array.isArray(valueTarget)) {