UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

695 lines (680 loc) • 28.5 kB
'use strict'; 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