framer-motion
Version:
A simple and powerful JavaScript animation library
695 lines (680 loc) • 28.5 kB
JavaScript
;
var jsxRuntime = require('react/jsx-runtime');
var motionUtils = require('motion-utils');
var React = require('react');
var index = require('./index-CfVlkVx9.js');
var motionDom = require('motion-dom');
function createMotionProxy(preloadedFeatures, createVisualElement) {
if (typeof Proxy === "undefined") {
return index.createMotionComponent;
}
/**
* A cache of generated `motion` components, e.g `motion.div`, `motion.input` etc.
* Rather than generating them anew every render.
*/
const componentCache = new Map();
const factory = (Component, options) => {
return index.createMotionComponent(Component, options, preloadedFeatures, createVisualElement);
};
/**
* Support for deprecated`motion(Component)` pattern
*/
const deprecatedFactoryFunction = (Component, options) => {
if (process.env.NODE_ENV !== "production") {
motionUtils.warnOnce(false, "motion() is deprecated. Use motion.create() instead.");
}
return factory(Component, options);
};
return new Proxy(deprecatedFactoryFunction, {
/**
* 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 (key === "create")
return factory;
/**
* If this element doesn't exist in the component cache, create it and cache.
*/
if (!componentCache.has(key)) {
componentCache.set(key, index.createMotionComponent(key, undefined, preloadedFeatures, createVisualElement));
}
return componentCache.get(key);
},
});
}
const ReorderContext =
/* @__PURE__ */ React.createContext(null);
function checkReorder(order, value, offset, velocity, axis, direction = "ltr") {
const index = order.findIndex((item) => item.value === value);
if (index === -1)
return order;
if (axis === "xy") {
const { layout } = order[index];
const center = {
x: motionDom.mixNumber(layout.x.min, layout.x.max, 0.5) + offset.x,
y: motionDom.mixNumber(layout.y.min, layout.y.max, 0.5) + offset.y,
};
const lines = getLines(order);
const sourceLine = lines.find((line) => line.items.includes(order[index]));
const targetLine = lines.reduce((closest, line) => distanceToLine(center.y, line) < distanceToLine(center.y, closest)
? line
: closest);
if (targetLine !== sourceLine) {
return moveToLine(order, index, center.x, targetLine, direction);
}
const currentDistance = distanceToBox(center, layout);
let target = -1;
let targetDistance = currentDistance;
order.forEach((item, targetIndex) => {
if (targetIndex === index)
return;
const distance = distanceToBox(center, item.layout);
if (distance < targetDistance) {
target = targetIndex;
targetDistance = distance;
}
});
return target === -1
? order
: motionUtils.moveItem(order, index, index + Math.sign(target - index));
}
if (!velocity[axis])
return order;
const nextOffset = velocity[axis] > 0 ? 1 : -1;
const nextItem = order[index + nextOffset];
if (!nextItem)
return order;
const item = order[index];
const itemLayout = item.layout[axis];
const nextLayout = nextItem.layout[axis];
const nextItemCenter = motionDom.mixNumber(nextLayout.min, nextLayout.max, 0.5);
if ((nextOffset === 1 && itemLayout.max + offset[axis] > nextItemCenter) ||
(nextOffset === -1 && itemLayout.min + offset[axis] < nextItemCenter)) {
return motionUtils.moveItem(order, index, index + nextOffset);
}
return order;
}
function getLines(order) {
const lines = [];
order.forEach((item) => {
const { min, max } = item.layout.y;
const line = lines[lines.length - 1];
if (!line || min >= line.max || max <= line.min) {
lines.push({ items: [item], min, max });
}
else {
line.items.push(item);
line.min = Math.min(line.min, min);
line.max = Math.max(line.max, max);
}
});
return lines;
}
function distanceToLine(y, line) {
return y < line.min ? line.min - y : y > line.max ? y - line.max : 0;
}
function moveToLine(order, index, x, line, direction) {
const remaining = order.filter((_, itemIndex) => itemIndex !== index);
const before = line.items.find((item) => {
const center = motionDom.mixNumber(item.layout.x.min, item.layout.x.max, 0.5);
return direction === "ltr" ? x < center : x > center;
});
const targetIndex = before
? remaining.indexOf(before)
: remaining.indexOf(line.items[line.items.length - 1]) + 1;
const nextOrder = [...remaining];
nextOrder.splice(targetIndex, 0, order[index]);
return nextOrder.every((item, itemIndex) => item === order[itemIndex])
? order
: nextOrder;
}
function distanceToBox(point, box) {
const x = Math.max(box.x.min - point.x, 0, point.x - box.x.max);
const y = Math.max(box.y.min - point.y, 0, point.y - box.y.max);
return x * x + y * y;
}
const isSeparated = (a, b) => a.max <= b.min || b.max <= a.min;
function detectAxis(layouts) {
let x = false;
let y = false;
for (let i = 0; i < layouts.length; i++) {
for (let j = i + 1; j < layouts.length; j++) {
x || (x = isSeparated(layouts[i].x, layouts[j].x));
y || (y = isSeparated(layouts[i].y, layouts[j].y));
if (x && y)
return "xy";
}
}
return x ? "x" : "y";
}
function ReorderGroupComponent({ children, as = "ul", axis: axisOverride, onReorder, values, onPointerDownCapture, onClickCapture, ...props }, externalRef, proxy) {
const Component = index.useConstant(() => proxy[as]);
const itemLayouts = React.useRef(new Map());
const [detectedAxis, setDetectedAxis] = React.useState("y");
const isReordering = React.useRef(false);
const hasReordered = React.useRef(false);
const groupRef = React.useRef(null);
const axis = axisOverride || detectedAxis;
process.env.NODE_ENV !== "production" &&
motionUtils.invariant(Boolean(values), "Reorder.Group must be provided a values prop", "reorder-values");
const valuesSet = new Set(values);
itemLayouts.current.forEach((_, value) => {
if (!valuesSet.has(value))
itemLayouts.current.delete(value);
});
const getOrder = () => values.flatMap((value) => {
const layout = itemLayouts.current.get(value);
return layout ? [{ value, layout }] : [];
});
const isRTL = () => groupRef.current?.ownerDocument.defaultView?.getComputedStyle(groupRef.current).direction === "rtl";
/**
* Write the new order of the measured items back into the slots they
* hold in `values`, leaving unmeasured (e.g. virtualized) values in place.
*/
const commit = (newOrder, moved) => {
let i = 0;
const newValues = values.map((value) => itemLayouts.current.has(value) ? newOrder[i++].value : value);
newValues.some((value, j) => value !== values[j]) &&
onReorder(newValues, moved);
};
const context = {
axis,
proxy,
groupRef,
hasReordered,
/**
* Space grabs and drops the item, arrows along `axis` move
* the grabbed item one place (Left and Right swap in RTL) and Escape
* restores the order in `from`, the values when it was grabbed. Keys
* from children or with modifiers (Alt+Left etc.) are left alone.
*/
onKeyDown: (event, item, from, grab) => {
const { key, currentTarget } = event;
if (event.defaultPrevented ||
event.target !== currentTarget ||
event.altKey ||
event.ctrlKey ||
event.metaKey) {
return;
}
const order = getOrder();
const keyIndex = [
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"ArrowRight",
].indexOf(key);
const isX = keyIndex % 2;
if (key === " ") {
event.repeat || grab(currentTarget, from ? undefined : values);
}
else if (!from) {
return;
}
else if (key === "Escape") {
/**
* Values added since the grab keep their slots.
*/
const kept = from.flatMap((value) => order.filter((data) => data.value === value));
commit(order.map((data) => from.includes(data.value) ? kept.shift() : data), item);
grab(currentTarget);
}
else if (keyIndex < 0 ||
(axis !== "xy" && axis !== (isX ? "x" : "y"))) {
return;
}
else {
const index = order.findIndex(({ value }) => value === item);
const target = index + (keyIndex > 1 !== !!(isX && isRTL()) ? 1 : -1);
index > -1 &&
order[target] &&
commit(motionUtils.moveItem(order, index, target), item);
}
event.preventDefault();
},
registerItem: (value, layout) => {
itemLayouts.current.set(value, layout);
if (!axisOverride) {
const nextAxis = detectAxis(values.flatMap((itemValue) => {
const itemLayout = itemLayouts.current.get(itemValue);
return itemLayout ? [itemLayout] : [];
}));
if (nextAxis !== detectedAxis)
setDetectedAxis(nextAxis);
}
},
updateOrder: (item, offset, velocity) => {
if (isReordering.current)
return;
const order = getOrder();
const newOrder = checkReorder(order, item, offset, velocity, axis, isRTL() ? "rtl" : "ltr");
if (order !== newOrder) {
isReordering.current = true;
hasReordered.current = true;
commit(newOrder, item);
}
},
};
/**
* Only clear the guard once the new order has been rendered. Unrelated
* commits (useTransition's isPending, stale renders in front of
* useDeferredValue) would otherwise re-enable onReorder while the
* previous order is still pending.
*/
React.useEffect(() => {
isReordering.current = false;
}, [values]);
// Combine refs if external ref is provided
const setRef = (element) => {
groupRef.current = element;
if (typeof externalRef === "function") {
externalRef(element);
}
else if (externalRef) {
externalRef.current =
element;
}
};
/**
* Disable browser scroll anchoring on the group container.
* When items reorder, scroll anchoring can cause the browser to adjust
* the scroll position, which interferes with drag position calculations.
*/
const groupStyle = {
overflowAnchor: "none",
...props.style,
};
return (jsxRuntime.jsx(Component, { ...props, style: groupStyle, ref: setRef,
/**
* A release that ends a reordering drag isn't a click. Browsers
* only click an element holding both the press and release
* targets, so whether it was clicked depended on which nodes
* React moved, and so on drag direction (#2682): Chrome drops
* the click if the dragged item's node moved, others may click
* an ancestor. Swallow it here, above every item, either way.
* The flag is cleared when the drag ends, a frame after the
* release's click, or on the next press. Clicks with no
* pointer press behind them (detail 0, e.g. keyboard) are left
* alone.
*/
onPointerDownCapture: (event) => {
hasReordered.current = false;
onPointerDownCapture?.(event);
}, onClickCapture: (event) => {
if (hasReordered.current && event.detail) {
hasReordered.current = false;
event.stopPropagation();
event.preventDefault();
}
onClickCapture?.(event);
}, ignoreStrict: true, children: jsxRuntime.jsx(ReorderContext.Provider, { value: context, children: children }) }));
}
/**
* Creates `Reorder.Group` rendering the components of `proxy`: `motion`,
* or `m` to get its features from `LazyMotion`. Its items render the same.
*/
const createReorderGroup = (proxy) => React.forwardRef((props, ref) => ReorderGroupComponent(props, ref, proxy));
/**
* 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 = index.useConstant(() => motionDom.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(index.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.
*/
index.useIsomorphicLayoutEffect(() => {
const scheduleUpdate = () => motionDom.frame.preRender(updateValue, false, true);
const subscriptions = values.map((v) => v.on("change", scheduleUpdate));
/**
* Catch up on changes made while unsubscribed. A source can change
* between render and this subscription (e.g. in a child's layout
* effect), and Strict Mode or <Activity> can re-run this effect
* without a render after the cleanup cancelled a scheduled update.
*/
updateValue();
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
motionDom.cancelFrame(updateValue);
};
});
return value;
}
function useComputed(compute) {
/**
* Open session of collectMotionValues. Any MotionValue that calls get()
* will be saved into this array.
*/
motionDom.collectMotionValues.current = [];
compute();
const value = useCombineMotionValues(motionDom.collectMotionValues.current, compute);
/**
* Synchronously close session of collectMotionValues.
*/
motionDom.collectMotionValues.current = undefined;
return value;
}
function useTransform(input, inputRangeOrTransformer, outputRangeOrMap, options) {
if (typeof input === "function") {
return useComputed(input);
}
/**
* Detect if outputRangeOrMap is an output map (object with keys)
* rather than an output range (array).
*/
const isOutputMap = outputRangeOrMap !== undefined &&
!Array.isArray(outputRangeOrMap) &&
typeof inputRangeOrTransformer !== "function";
if (isOutputMap) {
return useMapTransform(input, inputRangeOrTransformer, outputRangeOrMap, options);
}
const outputRange = outputRangeOrMap;
const transformer = typeof inputRangeOrTransformer === "function"
? inputRangeOrTransformer
: motionDom.transform(inputRangeOrTransformer, outputRange, options);
const result = Array.isArray(input)
? useListTransform(input, transformer)
: useListTransform([input], ([latest]) => transformer(latest));
const inputAccelerate = !Array.isArray(input)
? input.accelerate
: undefined;
if (inputAccelerate &&
!inputAccelerate.isTransformed &&
typeof inputRangeOrTransformer !== "function" &&
Array.isArray(outputRangeOrMap) &&
options?.clamp !== false) {
const ease = options?.ease;
/**
* WAAPI fills missing 0 and 1 offsets with the underlying value, so
* hold the end values to match the clamped transform.
*/
result.accelerate = {
...inputAccelerate,
times: [0, ...inputRangeOrTransformer, 1],
keyframes: [
outputRangeOrMap[0],
...outputRangeOrMap,
outputRangeOrMap[outputRangeOrMap.length - 1],
],
isTransformed: true,
...(ease
? { ease: Array.isArray(ease) ? [ease[0], ...ease] : ease }
: {}),
};
}
return result;
}
function useListTransform(values, transformer) {
const latest = index.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 useMapTransform(inputValue, inputRange, outputMap, options) {
/**
* Capture keys once to ensure hooks are called in consistent order.
*/
const keys = index.useConstant(() => Object.keys(outputMap));
const output = index.useConstant(() => ({}));
for (const key of keys) {
output[key] = useTransform(inputValue, inputRange, outputMap[key], options);
}
return output;
}
const threshold = 50;
// Pixels per millisecond (25px per frame at 60fps)
const maxSpeed = 1.5;
const overflowStyles = new Set(["auto", "scroll"]);
// Track initial scroll limits per scrollable element (Bug 1 fix)
const initialScrollLimits = new WeakMap();
const activeScrollEdge = new WeakMap();
// Track which group element is currently dragging to clear state on end
let currentGroupElement = null;
function resetAutoScrollState() {
if (currentGroupElement) {
const scrollableAncestor = findScrollableAncestor(currentGroupElement, "y");
if (scrollableAncestor) {
activeScrollEdge.delete(scrollableAncestor);
initialScrollLimits.delete(scrollableAncestor);
}
// Also try x axis
const scrollableAncestorX = findScrollableAncestor(currentGroupElement, "x");
if (scrollableAncestorX && scrollableAncestorX !== scrollableAncestor) {
activeScrollEdge.delete(scrollableAncestorX);
initialScrollLimits.delete(scrollableAncestorX);
}
currentGroupElement = null;
}
}
function isScrollableElement(element, axis) {
const style = getComputedStyle(element);
const overflow = axis === "x" ? style.overflowX : style.overflowY;
const isDocumentScroll = element === document.body ||
element === document.documentElement;
return overflowStyles.has(overflow) || isDocumentScroll;
}
function findScrollableAncestor(element, axis) {
let current = element?.parentElement;
while (current) {
if (isScrollableElement(current, axis)) {
return current;
}
current = current.parentElement;
}
return null;
}
/**
* Speed ramps up towards the edge and is capped at maxSpeed once the
* pointer reaches or passes it. This runs from onDrag, within a frame, so
* scale by the frame's elapsed time to scroll at the same speed whatever
* the display's refresh rate.
*/
function scrollSpeed(distanceFromEdge) {
const intensity = Math.min(1, 1 - distanceFromEdge / threshold);
return maxSpeed * intensity * intensity * motionDom.frameData.delta;
}
function getScrollAmount(pointerPosition, scrollElement, axis) {
const rect = scrollElement.getBoundingClientRect();
const start = axis === "x" ? Math.max(0, rect.left) : Math.max(0, rect.top);
const end = axis === "x" ? Math.min(window.innerWidth, rect.right) : Math.min(window.innerHeight, rect.bottom);
const distanceFromStart = pointerPosition - start;
const distanceFromEnd = end - pointerPosition;
if (distanceFromStart < threshold) {
return { amount: -scrollSpeed(distanceFromStart), edge: "start" };
}
else if (distanceFromEnd < threshold) {
return { amount: scrollSpeed(distanceFromEnd), edge: "end" };
}
return { amount: 0, edge: null };
}
function autoScrollIfNeeded(groupElement, pointerPosition, axis, velocity) {
if (!groupElement)
return;
// Track the group element for cleanup
currentGroupElement = groupElement;
const scrollableAncestor = findScrollableAncestor(groupElement, axis);
if (!scrollableAncestor)
return;
// Convert pointer position from page coordinates to viewport coordinates.
// The gesture system uses pageX/pageY but getBoundingClientRect() returns
// viewport-relative coordinates, so we need to account for page scroll.
const viewportPointerPosition = pointerPosition - (axis === "x" ? window.scrollX : window.scrollY);
const { amount: scrollAmount, edge } = getScrollAmount(viewportPointerPosition, scrollableAncestor, axis);
// If not in any threshold zone, clear all state
if (edge === null) {
activeScrollEdge.delete(scrollableAncestor);
initialScrollLimits.delete(scrollableAncestor);
return;
}
const currentActiveEdge = activeScrollEdge.get(scrollableAncestor);
const isDocumentScroll = scrollableAncestor === document.body ||
scrollableAncestor === document.documentElement;
// If not currently scrolling this edge, check velocity to see if we should start
if (currentActiveEdge !== edge) {
// Only start scrolling if velocity is towards the edge
const shouldStart = (edge === "start" && velocity < 0) ||
(edge === "end" && velocity > 0);
if (!shouldStart)
return;
// Activate this edge
activeScrollEdge.set(scrollableAncestor, edge);
// Record initial scroll limit (prevents infinite scroll)
const maxScroll = axis === "x"
? scrollableAncestor.scrollWidth - (isDocumentScroll ? window.innerWidth : scrollableAncestor.clientWidth)
: scrollableAncestor.scrollHeight - (isDocumentScroll ? window.innerHeight : scrollableAncestor.clientHeight);
initialScrollLimits.set(scrollableAncestor, maxScroll);
}
// Cap scrolling at initial limit (prevents infinite scroll)
if (scrollAmount > 0) {
const initialLimit = initialScrollLimits.get(scrollableAncestor);
const currentScroll = axis === "x"
? (isDocumentScroll ? window.scrollX : scrollableAncestor.scrollLeft)
: (isDocumentScroll ? window.scrollY : scrollableAncestor.scrollTop);
if (currentScroll >= initialLimit)
return;
}
// Apply scroll
if (axis === "x") {
if (isDocumentScroll) {
window.scrollBy({ left: scrollAmount });
}
else {
scrollableAncestor.scrollLeft += scrollAmount;
}
}
else {
if (isDocumentScroll) {
window.scrollBy({ top: scrollAmount });
}
else {
scrollableAncestor.scrollTop += scrollAmount;
}
}
}
function useDefaultMotionValue(value, defaultValue = 0) {
return motionDom.isMotionValue(value) ? value : useMotionValue(defaultValue);
}
function ReorderItemComponent({ children, style = {}, value, as = "li", onDrag, onDragEnd, onKeyDown, onBlur, layout = true, ...props }, externalRef) {
const context = React.useContext(ReorderContext);
process.env.NODE_ENV !== "production" &&
motionUtils.invariant(Boolean(context), "Reorder.Item must be a child of Reorder.Group", "reorder-item-child");
/**
* Render the group's kind of component, `motion` or `m`.
*/
const Component = index.useConstant(() => context.proxy[as]);
/**
* The group's values when this item was grabbed with the keyboard, for
* Escape to restore. Set only while grabbed.
*/
const [grabbedFrom, setGrabbedFrom] = React.useState();
const point = {
x: useDefaultMotionValue(style.x),
y: useDefaultMotionValue(style.y),
};
const zIndex = useTransform([point.x, point.y], ([latestX, latestY]) => latestX || latestY ? 1 : "unset");
const { axis, registerItem, updateOrder, groupRef, hasReordered, onKeyDown: handleKeyDown, } = context;
const dragAxis = axis === "xy" ? true : axis;
/**
* A keyboard grab is a drag without a pointer, so it shares whileDrag
* (but not the drag callbacks, which need a pointer and PanInfo).
*/
const grab = (element, from) => {
setGrabbedFrom(from);
motionDom.visualElementStore
.get(element)
?.animationState?.setActive("whileDrag", !!from);
};
return (jsxRuntime.jsx(Component, { drag: dragAxis, tabIndex: 0, ...props, dragSnapToOrigin: true, style: { ...style, x: point.x, y: point.y, zIndex }, layout: layout, onDrag: (event, gesturePoint) => {
const { velocity, point: pointerPoint } = gesturePoint;
const offset = { x: point.x.get(), y: point.y.get() };
updateOrder(value, offset, velocity);
const scrollAxis = axis === "xy"
? Math.abs(velocity.x) > Math.abs(velocity.y)
? "x"
: "y"
: axis;
autoScrollIfNeeded(groupRef.current, pointerPoint[scrollAxis], scrollAxis, velocity[scrollAxis]);
onDrag && onDrag(event, gesturePoint);
}, onDragEnd: (event, gesturePoint) => {
// Runs a frame after release, so after the release's click
hasReordered.current = false;
resetAutoScrollState();
onDragEnd && onDragEnd(event, gesturePoint);
}, onKeyDown: (event) => {
onKeyDown?.(event);
handleKeyDown(event, value, grabbedFrom, grab);
},
/**
* A move blurs the item, but that blur is never dispatched:
* React DOM disables its events while it commits, and restores
* focus to the item before re-enabling them. So any blur that
* reaches here is focus really leaving, which drops the item.
*/
onBlur: (event) => {
onBlur?.(event);
grabbedFrom && grab(event.currentTarget);
}, onLayoutMeasure: (measured) => {
registerItem(value, measured);
}, ref: externalRef, ignoreStrict: true, children: children }));
}
const ReorderItem = /*@__PURE__*/ React.forwardRef(ReorderItemComponent);
const m = /*@__PURE__*/ createMotionProxy();
exports.ReorderItem = ReorderItem;
exports.createMotionProxy = createMotionProxy;
exports.createReorderGroup = createReorderGroup;
exports.m = m;
exports.useCombineMotionValues = useCombineMotionValues;
exports.useComputed = useComputed;
exports.useMotionValue = useMotionValue;
exports.useTransform = useTransform;
//# sourceMappingURL=proxy-BFdh51TC.js.map