framer-motion
Version:
A simple and powerful JavaScript animation library
1,166 lines (1,148 loc) • 47.5 kB
JavaScript
;
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