UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1,166 lines (1,148 loc) • 47.5 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var motionUtils = require('motion-utils'); var React = require('react'); var index = require('./index-DI6GPImL.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 ReorderBoardContext = /* @__PURE__ */ React.createContext(null); 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 point = { x: center(layout.x) + offset.x, y: center(layout.y) + offset.y, }; const lines = getLines(order); const sourceLine = lines.find((line) => line.items.includes(order[index])); const targetLine = lines.reduce((closest, line) => distanceToLine(point.y, line) < distanceToLine(point.y, closest) ? line : closest); if (targetLine !== sourceLine) { return moveToLine(order, index, point.x, targetLine, direction); } const currentDistance = distanceToBox(point, layout); let target = -1; let targetDistance = currentDistance; order.forEach((item, targetIndex) => { if (targetIndex === index) return; const distance = distanceToBox(point, 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 = center(nextLayout); if ((nextOffset === 1 && itemLayout.max + offset[axis] > nextItemCenter) || (nextOffset === -1 && itemLayout.min + offset[axis] < nextItemCenter)) { return motionUtils.moveItem(order, index, index + nextOffset); } return order; } /** * The index a key takes `order[index]` to, or -1 if there's nowhere to go, * `key` being its index in Up, Left, Down, Right, Home, End. Left and Right * go along the order (swapped in RTL). On the "xy" axis, Up and Down go to * the row above or below, to the item there nearest across. Home and End * go to either end. */ function keyTarget(order, index, key, axis, isRTL) { const step = key > 1 !== !!(key % 2 && isRTL) ? 1 : -1; let target = key > 3 ? (key - 4) * (order.length - 1) : index + step; if (axis === "xy" && key < 4 && !(key % 2) && index > -1) { /** * Rows top to bottom as drawn, which isn't their order with * flex-wrap: wrap-reverse. */ const lines = getLines(order).sort((a, b) => a.min - b.min); const line = lines[lines.findIndex(({ items }) => items.includes(order[index])) + step]; const distance = (item) => Math.abs(center(item.layout.x) - center(order[index].layout.x)); target = line ? order.indexOf(line.items.reduce((nearest, item) => distance(item) < distance(nearest) ? item : nearest)) : -1; } return index > -1 && target !== index && order[target] ? target : -1; } const center = ({ min, max }) => motionDom.mixNumber(min, max, 0.5); 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 itemCenter = center(item.layout.x); return direction === "ltr" ? x < itemCenter : x > itemCenter; }); 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"; } /** * A callback ref that sets our own `ref` and an external one, a callback * ref or ref object. A callback ref's cleanup is called instead of the ref * with null, so it's returned wrapped to clear our own ref too. */ const mergeRefs = (ref, externalRef) => (value) => { ref.current = value; const cleanup = typeof externalRef === "function" ? externalRef(value) : externalRef && (externalRef.current = value); if (typeof cleanup === "function") { return () => { ref.current = null; cleanup(); }; } }; function ReorderGroupComponent({ children, as = "ul", axis: axisOverride, onReorder, values, onPointerDownCapture, onClickCapture, ...props }, externalRef, proxy) { const Component = index.useConstant(() => proxy[as]); const board = React.useContext(ReorderBoardContext); const items = React.useRef(new Map()); const elements = React.useRef(new Map()); const [active, setActive] = React.useState(); 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); for (const map of [items.current, elements.current]) { map.forEach((_, value) => valuesSet.has(value) || map.delete(value)); } const tabStop = valuesSet.has(active) ? active : values[0]; const isRendered = (value) => elements.current.get(value)?.current?.isConnected; /** * Only values in `values` can be the tab stop. */ const activate = (value) => valuesSet.has(value) && setActive(value); /** * If the tab stop isn't rendered (e.g. it's virtualized), the first * rendered item takes its place. Items check again when they unmount. */ const checkTabStop = () => { isRendered(tabStop) || activate(values.find(isRendered)); }; React.useEffect(checkTabStop); const getOrder = () => values.flatMap((value) => items.current.get(value) || []); 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) => items.current.has(value) ? newOrder[i++].value : value); newValues.some((value, j) => value !== values[j]) && onReorder(newValues, moved); }; const context = { axis, proxy, groupRef, hasReordered, values, onReorder, getOrder, isTabStop: (value) => value === tabStop || !valuesSet.has(value), setActive: activate, checkTabStop, elements: elements.current, /** * Space grabs and drops the item, arrows along `axis` move * the grabbed item one place, or a row in a grid (see keyTarget), * or move focus that way if it isn't grabbed. Home and End go to * either end. Escape restores the order in `from`, the values when * it was grabbed. Keys from children or with modifiers (Alt+Left * etc.) are left alone. * * In a Reorder.Board, arrows across `axis`, or along it from either * end, move a grabbed item to the nearest group that way, and * Escape returns it to the group it was grabbed in. */ 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", "Home", "End", ].indexOf(key); const isX = keyIndex % 2; if (key === " ") { event.repeat || grab(currentTarget, from ? undefined : values); } else if (keyIndex < 0 && !(key === "Escape" && from)) { return; } else if (board?.isMoving(currentTarget)) ; else if (key === "Escape" && from) { /** * Drop it first: a move back to the group it was grabbed in * hands it on to the item that mounts there. */ grab(currentTarget); if (!board?.restore(context, item, currentTarget, from)) { /** * 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); } } else { const isAlongAxis = keyIndex > 3 || axis === "xy" || axis === (isX ? "x" : "y"); /** * Focus only moves between rendered items. */ const list = from ? order : order.filter(({ value }) => isRendered(value)); const index = list.findIndex(({ value }) => value === item); const target = isAlongAxis ? keyTarget(list, index, keyIndex, axis, !!isX && isRTL()) : -1; if (target > -1) { from ? commit(motionUtils.moveItem(order, index, target), item) : elements.current .get(list[target].value) ?.current?.focus(); } else if (!(from && keyIndex < 4 && board?.key(context, item, keyIndex, currentTarget, from)) && !isAlongAxis) { return; } } event.preventDefault(); }, registerItem: (value, layout, ref) => { items.current.set(value, { value, layout, ref }); if (!axisOverride) { const nextAxis = detectAxis(getOrder().map((data) => data.layout)); 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]); /** * Keep a Reorder.Board up to date with the committed values. */ index.useIsomorphicLayoutEffect(() => board?.add(context)); /** * 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: mergeRefs(groupRef, externalRef), /** * 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 && // WAAPI can't call a mixer !options?.mixer) { 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"]); /** * Per axis and scrollable element, for the current drag: the edge it's * auto-scrolling towards, "start" (top/left) or "end" (bottom/right), and * its scroll limit when that began (Bug 1 fix). A drag in a Reorder.Board * scrolls on both axes, often of one element (like the page), and can * scroll several, so they're kept per axis and all cleared when it ends. */ const scrollStates = { x: new Map(), y: new Map(), }; function resetAutoScrollState() { scrollStates.x.clear(); scrollStates.y.clear(); } function isScrollableElement(element, axis) { const style = getComputedStyle(element); const overflow = axis === "x" ? style.overflowX : style.overflowY; const isDocumentScroll = element === document.body || element === document.documentElement; /** * An element that scrolls on one axis computes overflow auto on both * (e.g. a column of a board that scrolls the other way), so it must also * have content to scroll on this one. */ return (isDocumentScroll || (overflowStyles.has(overflow) && (axis === "x" ? element.scrollWidth > element.clientWidth : element.scrollHeight > element.clientHeight))); } 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; 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); const states = scrollStates[axis]; // If not in any threshold zone, clear this axis' state if (edge === null) { states.delete(scrollableAncestor); return; } let state = states.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 (state?.edge !== 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, and record the initial scroll limit // (prevents infinite scroll) state = { edge, limit: axis === "x" ? scrollableAncestor.scrollWidth - (isDocumentScroll ? window.innerWidth : scrollableAncestor.clientWidth) : scrollableAncestor.scrollHeight - (isDocumentScroll ? window.innerHeight : scrollableAncestor.clientHeight), }; states.set(scrollableAncestor, state); } // Cap scrolling at initial limit (prevents infinite scroll) if (scrollAmount > 0) { const currentScroll = axis === "x" ? (isDocumentScroll ? window.scrollX : scrollableAncestor.scrollLeft) : (isDocumentScroll ? window.scrollY : scrollableAncestor.scrollTop); if (currentScroll >= state.limit) 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", onDragStart, onDrag, onDragEnd, onKeyDown, onFocus, onBlur, layout = true, ...props }, externalRef) { const context = React.useContext(ReorderContext); const board = React.useContext(ReorderBoardContext); const ref = React.useRef(null); 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. Only key and focus events * read it, so it's a ref: a key sent before React renders, as test * runners can, still sees the grab. */ const grabbedFrom = React.useRef(); 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, isTabStop, setActive, checkTabStop, elements, onKeyDown: handleKeyDown, } = context; /** * In a Reorder.Board, items can be dragged out of their group. */ const dragAxis = axis === "xy" || board ? true : axis; /** * Keep this item's element for the group to focus. */ elements.set(value, ref); React.useEffect(() => checkTabStop); /** * 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) => { grabbedFrom.current = from; from || board?.end(value); motionDom.visualElementStore .get(element) ?.animationState?.setActive("whileDrag", !!from); }; /** * If this item was just moved here from another group, carry on its * drag or keyboard grab. A drag carried on is the same gesture, so it * doesn't call onDragStart again. */ const isContinued = React.useRef(false); index.useIsomorphicLayoutEffect(() => { board?.receive(context, value, ref.current, grab) && (isContinued.current = true); }, []); return (jsxRuntime.jsx(Component, { drag: dragAxis, tabIndex: isTabStop(value) ? 0 : -1, ...props, dragSnapToOrigin: true, style: { ...style, x: point.x, y: point.y, zIndex }, layout: layout, onDragStart: (event, gesturePoint) => { isContinued.current ? (isContinued.current = false) : onDragStart?.(event, gesturePoint); }, onDrag: (event, gesturePoint) => { const { velocity, point: pointerPoint } = gesturePoint; const offset = { x: point.x.get(), y: point.y.get() }; board?.drag(context, value, ref.current, event) || updateOrder(value, offset, velocity); /** * In a Reorder.Board, scroll on both axes, e.g. across * columns as well as down them. Across them, scroll what's * around the columns: an item dragged out of its column * overflows it, so the column could scroll across too. */ const scrollAxes = board ? ["x", "y"] : [ axis === "xy" ? Math.abs(velocity.x) > Math.abs(velocity.y) ? "x" : "y" : axis, ]; for (const scrollAxis of scrollAxes) { autoScrollIfNeeded(board && !axis.includes(scrollAxis) ? board.column(context) : 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(); board?.end(value); onDragEnd && onDragEnd(event, gesturePoint); }, onKeyDown: (event) => { onKeyDown?.(event); handleKeyDown(event, value, grabbedFrom.current, grab); }, /** * The item focused last, or holding focus, is the group's tab * stop. */ onFocus: (event) => { onFocus?.(event); setActive(value); }, /** * 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.current && grab(event.currentTarget); }, onLayoutMeasure: (measured) => { registerItem(value, measured, ref); }, ref: mergeRefs(ref, externalRef), ignoreStrict: true, children: children })); } const ReorderItem = /*@__PURE__*/ React.forwardRef(ReorderItemComponent); const centre = (element) => { const { left, top, width, height } = element.getBoundingClientRect(); return { x: left + width / 2, y: top + height / 2 }; }; /** * Its centre in page coordinates. */ const pageCentre = (element) => { const { x, y } = centre(element); const { scrollX, scrollY } = element.ownerDocument.defaultView; return { x: x + scrollX, y: y + scrollY }; }; /** * Whether `point` comes before `element` in its group's order: before its * centre on `axis` or, on "xy", above its line or before its centre on it. */ function isBefore(point, element, axis, isRTL) { const { left, right, top, bottom } = element.getBoundingClientRect(); const x = isRTL ? point.x > (left + right) / 2 : point.x <= (left + right) / 2; const y = point.y <= (top + bottom) / 2; return axis === "x" ? x : axis === "y" ? y : point.y < top || (point.y < bottom && x); } /** * Whether one group is, or holds, the other: like a list of columns and the * list of cards in one of them. Items only move between unrelated groups. */ const isNested = (a, b) => { const x = a.groupRef.current; const y = b.groupRef.current; return x.contains(y) || y.contains(x); }; function createBoard(props) { const groups = new Set(); let handoff; /** * Whether `value` can move between these groups, if the app lets it. */ const canMove = (from, to, value) => !isNested(from, to) && props.current.canMove?.(value, from.values, to.values) !== false; /** * Where the latest keyboard grab began, for Escape to return it there. */ let origin; /** * Move `value` from one group to another: to `index` in its values or, * by default, the slot under the item's centre. */ const move = (from, to, value, element, props, index) => { const point = centre(element); const { values } = to; if (index === undefined) { /** * Only the items it shows: a group keeps unmounted (e.g. * virtualized) items' data while their values remain. */ const order = to .getOrder() .filter((item) => item.ref?.current?.isConnected); const isRTL = getComputedStyle(to.groupRef.current).direction === "rtl"; const next = order.find((item) => isBefore(point, item.ref.current, to.axis, isRTL)); const last = order[order.length - 1]; index = next ? values.indexOf(next.value) : last ? values.indexOf(last.value) + 1 : values.length; } handoff = { value, to: to.groupRef, element, centre: pageCentre(element), ...props, }; /** * The release that ends a drag lands over the new group. */ to.hasReordered.current = true; from.onReorder(from.values.filter((item) => item !== value), value); to.onReorder([...values.slice(0, index), value, ...values.slice(index)], value); }; return { add: (group) => { groups.add(group); return () => groups.delete(group); }, drag: (from, value, element, event) => { /** * The item can unmount mid-drag (e.g. the app removed it), while * its gesture carries on until release. */ if (!element?.isConnected) return false; /** * Wait for the item to arrive in its new group before moving * it again, keeping up with where it's dragged meanwhile, for * its drag to carry on from there. If the groups don't take * it, it stays here until this gesture ends. */ if (handoff?.controls?.isDragging) { if (handoff.value === value) { handoff.centre = pageCentre(element); handoff.event = event; } return true; } const point = centre(element); let to; for (const hit of element.ownerDocument.elementsFromPoint(point.x, point.y)) { // Skip the item, which is drawn above everything else if (element.contains(hit)) continue; for (const group of groups) { if (group.groupRef.current === hit) to = group; } if (to) break; } if (!to || !canMove(from, to, value)) return false; const visualElement = motionDom.visualElementStore.get(element); move(from, to, value, element, { controls: visualElement && motionDom.elementDragControls.get(visualElement), event, }); return true; }, key: (from, value, keyIndex, element, grabbed) => { const point = centre(element); const axis = keyIndex % 2 ? "x" : "y"; const direction = keyIndex > 1 ? 1 : -1; let to; let nearest = Infinity; for (const group of groups) { const target = group.groupRef.current; if (!target) continue; const { left, right, top, bottom, width, height } = target.getBoundingClientRect(); // Hidden, e.g. display: none if (!width && !height) continue; /** * Its near edge is past the item that way, so a group beside * this one isn't below its last item, whatever their heights. */ const edge = [bottom, right, top, left][keyIndex]; if ((edge - point[axis]) * direction <= 0) continue; const x = Math.max(left - point.x, 0, point.x - right); const y = Math.max(top - point.y, 0, point.y - bottom); if (x * x + y * y < nearest && canMove(from, group, value)) { nearest = x * x + y * y; to = group; } } if (!to) return false; if (origin?.from !== grabbed) { origin = { group: from.groupRef, from: grabbed }; } move(from, to, value, element, { keys: true, from: grabbed }); return true; }, restore: (from, value, element, grabbed) => { const to = origin?.from === grabbed && [...groups].find((group) => group.groupRef === origin.group); if (!to || to.groupRef === from.groupRef) return false; /** * Before the first value that followed it when it was grabbed, * and is still there. */ const after = grabbed.slice(grabbed.indexOf(value) + 1); const index = to.values.findIndex((item) => after.includes(item)); origin = undefined; move(from, to, value, element, { keys: true }, index < 0 ? to.values.length : index); return true; }, end: (value) => { if (handoff?.value === value) handoff = undefined; }, isMoving: (element) => handoff?.element === element, receive: (to, value, element, grab) => { const visualElement = motionDom.visualElementStore.get(element); const nextControls = visualElement && motionDom.elementDragControls.get(visualElement); /** * React 19's Strict Mode remounts a new item, which resets its * whileDrag, after its drag may already have carried on. */ nextControls?.isDragging && visualElement.animationState?.setActive("whileDrag", true); const latest = handoff; if (!latest || latest.value !== value || latest.to !== to.groupRef) { return false; } const { keys, from, controls } = latest; /** * Whether the groups moved the item, rather than copied it. */ const isMoved = !latest.element.isConnected; if (keys) { handoff = undefined; /** * Unless focus has moved on since, e.g. the groups took it * much later. */ const { activeElement, body } = element.ownerDocument; if (isMoved && (!activeElement || activeElement === body)) { element.focus(); grab(element, from); } return false; } if (!nextControls || !controls?.isDragging) return false; /** * Once the commit that moved the item has been measured, and * before it's drawn. Until then, the item's drag in its * previous group waits. */ motionDom.microtask.read(() => { if (handoff !== latest) return; handoff = undefined; /** * Start it where, and as, the item was dragged to (e.g. * mid-way through its whileDrag animation), and carry on * dragging it from there. Values are read from the item * first, for its whileDrag to return to on release. */ motionDom.visualElementStore .get(latest.element) ?.values.forEach((motionValue, key) => { const current = motionValue.get(); visualElement .getValue(key, visualElement.readValue(key, current)) ?.set(current); }); /** * Into the item's own coordinates, e.g. in a scaled parent. */ const transform = visualElement.getTransformPagePoint() || ((p) => p); const a = transform(latest.centre); const b = transform(pageCentre(element)); visualElement.getValue("x").set(a.x - b.x); visualElement.getValue("y").set(a.y - b.y); visualElement.render(); /** * If the item is still in its previous group, animate it * back to its slot there. */ isMoved ? controls.cancel() : controls.stop(); nextControls.start(latest.event, { immediate: true, }); }); /** * Its drag already started in its previous group. */ return isMoved; }, column: ({ groupRef }) => { let element = groupRef.current; const holdsOther = (ancestor) => [...groups].some((group) => group.groupRef !== groupRef && ancestor.contains(group.groupRef.current)); while (element?.parentElement?.parentElement && !holdsOther(element.parentElement)) { element = element.parentElement; } return element; }, }; } /** * Lets items be dragged between the `Reorder.Group`s inside it. * * ```jsx * <Reorder.Board> * <Reorder.Group values={todo} onReorder={setTodo}> * {todo.map((item) => <Reorder.Item key={item} value={item} />)} * </Reorder.Group> * <Reorder.Group values={done} onReorder={setDone}> * {done.map((item) => <Reorder.Item key={item} value={item} />)} * </Reorder.Group> * </Reorder.Board> * ``` * * When an item's centre is dragged over another group, it moves there: * the group it left calls `onReorder` without it, and the group it joined * calls `onReorder` with it, both passing it as the moved value. Their * other items animate to make room, and the drag carries on in the new * group. * * Grabbed with the keyboard, an item moves to the nearest group in the * direction of an arrow key across its group's axis, or along it from * either end. Escape returns it to the group and slot it was grabbed from. * * `canMove` can refuse a move before either group updates. * * Values must be unique across the groups. If the app keeps the groups' * values in one state, update it from the latest state (e.g. * `setColumns((columns) => ...)`), as both groups update it in turn. * * @public */ function ReorderBoard({ children, ...props }) { const latest = React.useRef(props); /** * In a layout effect, so a render React discards can't leave its * props here. */ index.useIsomorphicLayoutEffect(() => { latest.current = props; }); return (jsxRuntime.jsx(ReorderBoardContext.Provider, { value: index.useConstant(() => createBoard(latest)), children: children })); } const m = /*@__PURE__*/ createMotionProxy(); exports.ReorderBoard = ReorderBoard; 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-CPkHy6Pk.js.map