UNPKG

framer-motion

Version:

A simple and powerful JavaScript animation library

1,287 lines (1,252 loc) • 116 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); var featureBundle = require('./feature-bundle-BeDch4T0.js'); var motionDom = require('motion-dom'); var motionUtils = require('motion-utils'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React); /** * Taken from https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx */ /** * Set a given ref to a given value * This utility takes care of different types of refs: callback refs and RefObject(s) */ function setRef(ref, value) { if (typeof ref === "function") { return ref(value); } else if (ref !== null && ref !== undefined) { ref.current = value; } } /** * A utility to compose multiple refs together * Accepts callback refs and RefObject(s) */ function composeRefs(...refs) { return (node) => { let hasCleanup = false; const cleanups = refs.map((ref) => { const cleanup = setRef(ref, node); if (!hasCleanup && typeof cleanup === "function") { hasCleanup = true; } return cleanup; }); // React <19 will log an error to the console if a callback ref returns a // value. We don't use ref cleanups internally so this will only happen if a // user's ref callback returns a value, which we only expect if they are // using the cleanup functionality added in React 19. if (hasCleanup) { return () => { for (let i = 0; i < cleanups.length; i++) { const cleanup = cleanups[i]; if (typeof cleanup === "function") { cleanup(); } else { setRef(refs[i], null); } } }; } }; } /** * A custom hook that composes multiple refs * Accepts callback refs and RefObject(s) */ function useComposedRefs(...refs) { // eslint-disable-next-line react-hooks/exhaustive-deps return React__namespace.useCallback(composeRefs(...refs), refs); } /** * Measurement functionality has to be within a separate component * to leverage snapshot lifecycle. */ class PopChildMeasure extends React__namespace.Component { getSnapshotBeforeUpdate(prevProps) { const element = this.props.childRef.current; if (element && prevProps.isPresent && !this.props.isPresent) { const parent = element.offsetParent; const parentWidth = motionDom.isHTMLElement(parent) ? parent.offsetWidth || 0 : 0; const size = this.props.sizeRef.current; size.height = element.offsetHeight || 0; size.width = element.offsetWidth || 0; size.top = element.offsetTop; size.left = element.offsetLeft; size.right = parentWidth - size.width - size.left; } return null; } /** * Required with getSnapshotBeforeUpdate to stop React complaining. */ componentDidUpdate() { } render() { return this.props.children; } } function PopChild({ children, isPresent, anchorX, root }) { const id = React.useId(); const ref = React.useRef(null); const size = React.useRef({ width: 0, height: 0, top: 0, left: 0, right: 0, }); const { nonce } = React.useContext(featureBundle.MotionConfigContext); /** * In React 19, refs are passed via props.ref instead of element.ref. * We check props.ref first (React 19) and fall back to element.ref (React 18). */ const childRef = children.props?.ref ?? children?.ref; const composedRef = useComposedRefs(ref, childRef); /** * We create and inject a style block so we can apply this explicit * sizing in a non-destructive manner by just deleting the style block. * * We can't apply size via render as the measurement happens * in getSnapshotBeforeUpdate (post-render), likewise if we apply the * styles directly on the DOM node, we might be overwriting * styles set via the style prop. */ React.useInsertionEffect(() => { const { width, height, top, left, right } = size.current; if (isPresent || !ref.current || !width || !height) return; const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`; ref.current.dataset.motionPopId = id; const style = document.createElement("style"); if (nonce) style.nonce = nonce; const parent = root ?? document.head; parent.appendChild(style); if (style.sheet) { style.sheet.insertRule(` [data-motion-pop-id="${id}"] { position: absolute !important; width: ${width}px !important; height: ${height}px !important; ${x}px !important; top: ${top}px !important; } `); } return () => { if (parent.contains(style)) { parent.removeChild(style); } }; }, [isPresent]); return (jsxRuntime.jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React__namespace.cloneElement(children, { ref: composedRef }) })); } const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, root }) => { const presenceChildren = featureBundle.useConstant(newChildrenMap); const id = React.useId(); let isReusedContext = true; let context = React.useMemo(() => { isReusedContext = false; return { id, initial, isPresent, custom, onExitComplete: (childId) => { presenceChildren.set(childId, true); for (const isComplete of presenceChildren.values()) { if (!isComplete) return; // can stop searching when any is incomplete } onExitComplete && onExitComplete(); }, register: (childId) => { presenceChildren.set(childId, false); return () => presenceChildren.delete(childId); }, }; }, [isPresent, presenceChildren, onExitComplete]); /** * If the presence of a child affects the layout of the components around it, * we want to make a new context value to ensure they get re-rendered * so they can detect that layout change. */ if (presenceAffectsLayout && isReusedContext) { context = { ...context }; } React.useMemo(() => { presenceChildren.forEach((_, key) => presenceChildren.set(key, false)); }, [isPresent]); /** * If there's no `motion` components to fire exit animations, we want to remove this * component immediately. */ React__namespace.useEffect(() => { !isPresent && !presenceChildren.size && onExitComplete && onExitComplete(); }, [isPresent]); if (mode === "popLayout") { children = (jsxRuntime.jsx(PopChild, { isPresent: isPresent, anchorX: anchorX, root: root, children: children })); } return (jsxRuntime.jsx(featureBundle.PresenceContext.Provider, { value: context, children: children })); }; function newChildrenMap() { return new Map(); } const getChildKey = (child) => child.key || ""; function onlyElements(children) { const filtered = []; // We use forEach here instead of map as map mutates the component key by preprending `.$` React.Children.forEach(children, (child) => { if (React.isValidElement(child)) filtered.push(child); }); return filtered; } /** * `AnimatePresence` enables the animation of components that have been removed from the tree. * * When adding/removing more than a single child, every child **must** be given a unique `key` prop. * * Any `motion` components that have an `exit` property defined will animate out when removed from * the tree. * * ```jsx * import { motion, AnimatePresence } from 'framer-motion' * * export const Items = ({ items }) => ( * <AnimatePresence> * {items.map(item => ( * <motion.div * key={item.id} * initial={{ opacity: 0 }} * animate={{ opacity: 1 }} * exit={{ opacity: 0 }} * /> * ))} * </AnimatePresence> * ) * ``` * * You can sequence exit animations throughout a tree using variants. * * If a child contains multiple `motion` components with `exit` props, it will only unmount the child * once all `motion` components have finished animating out. Likewise, any components using * `usePresence` all need to call `safeToRemove`. * * @public */ const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", root }) => { const [isParentPresent, safeToRemove] = featureBundle.usePresence(propagate); /** * Filter any children that aren't ReactElements. We can only track components * between renders with a props.key. */ const presentChildren = React.useMemo(() => onlyElements(children), [children]); /** * Track the keys of the currently rendered children. This is used to * determine which children are exiting. */ const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey); /** * If `initial={false}` we only want to pass this to components in the first render. */ const isInitialRender = React.useRef(true); /** * A ref containing the currently present children. When all exit animations * are complete, we use this to re-render the component with the latest children * *committed* rather than the latest children *rendered*. */ const pendingPresentChildren = React.useRef(presentChildren); /** * Track which exiting children have finished animating out. */ const exitComplete = featureBundle.useConstant(() => new Map()); /** * Track which components are currently processing exit to prevent duplicate processing. */ const exitingComponents = React.useRef(new Set()); /** * Save children to render as React state. To ensure this component is concurrent-safe, * we check for exiting children via an effect. */ const [diffedChildren, setDiffedChildren] = React.useState(presentChildren); const [renderedChildren, setRenderedChildren] = React.useState(presentChildren); featureBundle.useIsomorphicLayoutEffect(() => { isInitialRender.current = false; pendingPresentChildren.current = presentChildren; /** * Update complete status of exiting children. */ for (let i = 0; i < renderedChildren.length; i++) { const key = getChildKey(renderedChildren[i]); if (!presentKeys.includes(key)) { if (exitComplete.get(key) !== true) { exitComplete.set(key, false); } } else { exitComplete.delete(key); exitingComponents.current.delete(key); } } }, [renderedChildren, presentKeys.length, presentKeys.join("-")]); const exitingChildren = []; if (presentChildren !== diffedChildren) { let nextChildren = [...presentChildren]; /** * Loop through all the currently rendered components and decide which * are exiting. */ for (let i = 0; i < renderedChildren.length; i++) { const child = renderedChildren[i]; const key = getChildKey(child); if (!presentKeys.includes(key)) { nextChildren.splice(i, 0, child); exitingChildren.push(child); } } /** * If we're in "wait" mode, and we have exiting children, we want to * only render these until they've all exited. */ if (mode === "wait" && exitingChildren.length) { nextChildren = exitingChildren; } setRenderedChildren(onlyElements(nextChildren)); setDiffedChildren(presentChildren); /** * Early return to ensure once we've set state with the latest diffed * children, we can immediately re-render. */ return null; } if (process.env.NODE_ENV !== "production" && mode === "wait" && renderedChildren.length > 1) { console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`); } /** * If we've been provided a forceRender function by the LayoutGroupContext, * we can use it to force a re-render amongst all surrounding components once * all components have finished animating out. */ const { forceRender } = React.useContext(featureBundle.LayoutGroupContext); return (jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderedChildren.map((child) => { const key = getChildKey(child); const isPresent = propagate && !isParentPresent ? false : presentChildren === renderedChildren || presentKeys.includes(key); const onExit = () => { if (exitingComponents.current.has(key)) { return; } exitingComponents.current.add(key); if (exitComplete.has(key)) { exitComplete.set(key, true); } else { return; } let isEveryExitComplete = true; exitComplete.forEach((isExitComplete) => { if (!isExitComplete) isEveryExitComplete = false; }); if (isEveryExitComplete) { forceRender?.(); setRenderedChildren(pendingPresentChildren.current); propagate && safeToRemove?.(); onExitComplete && onExitComplete(); } }; return (jsxRuntime.jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial ? undefined : false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, root: root, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key)); }) })); }; /** * Note: Still used by components generated by old versions of Framer * * @deprecated */ const DeprecatedLayoutGroupContext = React.createContext(null); function useIsMounted() { const isMounted = React.useRef(false); featureBundle.useIsomorphicLayoutEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return isMounted; } function useForceUpdate() { const isMounted = useIsMounted(); const [forcedRenderCount, setForcedRenderCount] = React.useState(0); const forceRender = React.useCallback(() => { isMounted.current && setForcedRenderCount(forcedRenderCount + 1); }, [forcedRenderCount]); /** * Defer this to the end of the next animation frame in case there are multiple * synchronous calls. */ const deferredForceRender = React.useCallback(() => motionDom.frame.postRender(forceRender), [forceRender]); return [deferredForceRender, forcedRenderCount]; } const shouldInheritGroup = (inherit) => inherit === true; const shouldInheritId = (inherit) => shouldInheritGroup(inherit === true) || inherit === "id"; const LayoutGroup = ({ children, id, inherit = true }) => { const layoutGroupContext = React.useContext(featureBundle.LayoutGroupContext); const deprecatedLayoutGroupContext = React.useContext(DeprecatedLayoutGroupContext); const [forceRender, key] = useForceUpdate(); const context = React.useRef(null); const upstreamId = layoutGroupContext.id || deprecatedLayoutGroupContext; if (context.current === null) { if (shouldInheritId(inherit) && upstreamId) { id = id ? upstreamId + "-" + id : upstreamId; } context.current = { id, group: shouldInheritGroup(inherit) ? layoutGroupContext.group || motionDom.nodeGroup() : motionDom.nodeGroup(), }; } const memoizedContext = React.useMemo(() => ({ ...context.current, forceRender }), [key]); return (jsxRuntime.jsx(featureBundle.LayoutGroupContext.Provider, { value: memoizedContext, children: children })); }; /** * Used in conjunction with the `m` component to reduce bundle size. * * `m` is a version of the `motion` component that only loads functionality * critical for the initial render. * * `LazyMotion` can then be used to either synchronously or asynchronously * load animation and gesture support. * * ```jsx * // Synchronous loading * import { LazyMotion, m, domAnimation } from "framer-motion" * * function App() { * return ( * <LazyMotion features={domAnimation}> * <m.div animate={{ scale: 2 }} /> * </LazyMotion> * ) * } * * // Asynchronous loading * import { LazyMotion, m } from "framer-motion" * * function App() { * return ( * <LazyMotion features={() => import('./path/to/domAnimation')}> * <m.div animate={{ scale: 2 }} /> * </LazyMotion> * ) * } * ``` * * @public */ function LazyMotion({ children, features, strict = false }) { const [, setIsLoaded] = React.useState(!isLazyBundle(features)); const loadedRenderer = React.useRef(undefined); /** * If this is a synchronous load, load features immediately */ if (!isLazyBundle(features)) { const { renderer, ...loadedFeatures } = features; loadedRenderer.current = renderer; featureBundle.loadFeatures(loadedFeatures); } React.useEffect(() => { if (isLazyBundle(features)) { features().then(({ renderer, ...loadedFeatures }) => { featureBundle.loadFeatures(loadedFeatures); loadedRenderer.current = renderer; setIsLoaded(true); }); } }, []); return (jsxRuntime.jsx(featureBundle.LazyContext.Provider, { value: { renderer: loadedRenderer.current, strict }, children: children })); } function isLazyBundle(features) { return typeof features === "function"; } /** * `MotionConfig` is used to set configuration options for all children `motion` components. * * ```jsx * import { motion, MotionConfig } from "framer-motion" * * export function App() { * return ( * <MotionConfig transition={{ type: "spring" }}> * <motion.div animate={{ x: 100 }} /> * </MotionConfig> * ) * } * ``` * * @public */ function MotionConfig({ children, isValidProp, ...config }) { isValidProp && featureBundle.loadExternalIsValidProp(isValidProp); /** * Inherit props from any parent MotionConfig components */ config = { ...React.useContext(featureBundle.MotionConfigContext), ...config }; /** * Don't allow isStatic to change between renders as it affects how many hooks * motion components fire. */ config.isStatic = featureBundle.useConstant(() => config.isStatic); /** * Creating a new config context object will re-render every `motion` component * every time it renders. So we only want to create a new one sparingly. */ const context = React.useMemo(() => config, [ JSON.stringify(config.transition), config.transformPagePoint, config.reducedMotion, ]); return (jsxRuntime.jsx(featureBundle.MotionConfigContext.Provider, { value: context, children: children })); } const ReorderContext = React.createContext(null); function createMotionProxy(preloadedFeatures, createVisualElement) { if (typeof Proxy === "undefined") { return featureBundle.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 featureBundle.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, featureBundle.createMotionComponent(key, undefined, preloadedFeatures, createVisualElement)); } return componentCache.get(key); }, }); } const motion = /*@__PURE__*/ createMotionProxy(featureBundle.featureBundle, featureBundle.createDomVisualElement); function checkReorder(order, value, offset, velocity) { if (!velocity) return order; const index = order.findIndex((item) => item.value === value); if (index === -1) return order; const nextOffset = velocity > 0 ? 1 : -1; const nextItem = order[index + nextOffset]; if (!nextItem) return order; const item = order[index]; const nextLayout = nextItem.layout; const nextItemCenter = motionDom.mixNumber(nextLayout.min, nextLayout.max, 0.5); if ((nextOffset === 1 && item.layout.max + offset > nextItemCenter) || (nextOffset === -1 && item.layout.min + offset < nextItemCenter)) { return motionUtils.moveItem(order, index, index + nextOffset); } return order; } function ReorderGroupComponent({ children, as = "ul", axis = "y", onReorder, values, ...props }, externalRef) { const Component = featureBundle.useConstant(() => motion[as]); const order = []; const isReordering = React.useRef(false); const groupRef = React.useRef(null); motionUtils.invariant(Boolean(values), "Reorder.Group must be provided a values prop", "reorder-values"); const context = { axis, groupRef, registerItem: (value, layout) => { // If the entry was already added, update it rather than adding it again const idx = order.findIndex((entry) => value === entry.value); if (idx !== -1) { order[idx].layout = layout[axis]; } else { order.push({ value: value, layout: layout[axis] }); } order.sort(compareMin); }, updateOrder: (item, offset, velocity) => { if (isReordering.current) return; const newOrder = checkReorder(order, item, offset, velocity); if (order !== newOrder) { isReordering.current = true; onReorder(newOrder .map(getValue) .filter((value) => values.indexOf(value) !== -1)); } }, }; React.useEffect(() => { isReordering.current = false; }); // 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, ignoreStrict: true, children: jsxRuntime.jsx(ReorderContext.Provider, { value: context, children: children }) })); } const ReorderGroup = /*@__PURE__*/ React.forwardRef(ReorderGroupComponent); function getValue(item) { return item.value; } function compareMin(a, b) { return a.layout.min - b.layout.min; } /** * 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 = featureBundle.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(featureBundle.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. */ featureBundle.useIsomorphicLayoutEffect(() => { const scheduleUpdate = () => motionDom.frame.preRender(updateValue, false, true); const subscriptions = values.map((v) => v.on("change", scheduleUpdate)); 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); return Array.isArray(input) ? useListTransform(input, transformer) : useListTransform([input], ([latest]) => transformer(latest)); } function useListTransform(values, transformer) { const latest = featureBundle.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 = featureBundle.useConstant(() => Object.keys(outputMap)); const output = featureBundle.useConstant(() => ({})); for (const key of keys) { output[key] = useTransform(inputValue, inputRange, outputMap[key], options); } return output; } const threshold = 50; const maxSpeed = 25; 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; return overflowStyles.has(overflow); } function findScrollableAncestor(element, axis) { let current = element?.parentElement; while (current) { if (isScrollableElement(current, axis)) { return current; } current = current.parentElement; } return null; } function getScrollAmount(pointerPosition, scrollElement, axis) { const rect = scrollElement.getBoundingClientRect(); const start = axis === "x" ? rect.left : rect.top; const end = axis === "x" ? rect.right : rect.bottom; const distanceFromStart = pointerPosition - start; const distanceFromEnd = end - pointerPosition; if (distanceFromStart < threshold) { const intensity = 1 - distanceFromStart / threshold; return { amount: -maxSpeed * intensity * intensity, edge: "start" }; } else if (distanceFromEnd < threshold) { const intensity = 1 - distanceFromEnd / threshold; return { amount: maxSpeed * intensity * intensity, 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; const { amount: scrollAmount, edge } = getScrollAmount(pointerPosition, 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); // 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 - scrollableAncestor.clientWidth : scrollableAncestor.scrollHeight - 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" ? scrollableAncestor.scrollLeft : scrollableAncestor.scrollTop; if (currentScroll >= initialLimit) return; } // Apply scroll if (axis === "x") { scrollableAncestor.scrollLeft += 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, layout = true, ...props }, externalRef) { const Component = featureBundle.useConstant(() => motion[as]); const context = React.useContext(ReorderContext); const point = { x: useDefaultMotionValue(style.x), y: useDefaultMotionValue(style.y), }; const zIndex = useTransform([point.x, point.y], ([latestX, latestY]) => latestX || latestY ? 1 : "unset"); motionUtils.invariant(Boolean(context), "Reorder.Item must be a child of Reorder.Group", "reorder-item-child"); const { axis, registerItem, updateOrder, groupRef } = context; return (jsxRuntime.jsx(Component, { drag: axis, ...props, dragSnapToOrigin: true, style: { ...style, x: point.x, y: point.y, zIndex }, layout: layout, onDrag: (event, gesturePoint) => { const { velocity, point: pointerPoint } = gesturePoint; const offset = point[axis].get(); // Always attempt to update order - checkReorder handles the logic updateOrder(value, offset, velocity[axis]); autoScrollIfNeeded(groupRef.current, pointerPoint[axis], axis, velocity[axis]); onDrag && onDrag(event, gesturePoint); }, onDragEnd: (event, gesturePoint) => { resetAutoScrollState(); onDragEnd && onDragEnd(event, gesturePoint); }, onLayoutMeasure: (measured) => { registerItem(value, measured); }, ref: externalRef, ignoreStrict: true, children: children })); } const ReorderItem = /*@__PURE__*/ React.forwardRef(ReorderItemComponent); var namespace = /*#__PURE__*/Object.freeze({ __proto__: null, Group: ReorderGroup, Item: ReorderItem }); function isDOMKeyframes(keyframes) { return typeof keyframes === "object" && !Array.isArray(keyframes); } function resolveSubjects(subject, keyframes, scope, selectorCache) { if (typeof subject === "string" && isDOMKeyframes(keyframes)) { return motionDom.resolveElements(subject, scope, selectorCache); } else if (subject instanceof NodeList) { return Array.from(subject); } else if (Array.isArray(subject)) { return subject; } else { return [subject]; } } function calculateRepeatDuration(duration, repeat, _repeatDelay) { return duration * (repeat + 1); } /** * Given a absolute or relative time definition and current/prev time state of the sequence, * calculate an absolute time for the next keyframes. */ function calcNextTime(current, next, prev, labels) { if (typeof next === "number") { return next; } else if (next.startsWith("-") || next.startsWith("+")) { return Math.max(0, current + parseFloat(next)); } else if (next === "<") { return prev; } else if (next.startsWith("<")) { return Math.max(0, prev + parseFloat(next.slice(1))); } else { return labels.get(next) ?? current; } } function eraseKeyframes(sequence, startTime, endTime) { for (let i = 0; i < sequence.length; i++) { const keyframe = sequence[i]; if (keyframe.at > startTime && keyframe.at < endTime) { motionUtils.removeItem(sequence, keyframe); // If we remove this item we have to push the pointer back one i--; } } } function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) { /** * Erase every existing value between currentTime and targetTime, * this will essentially splice this timeline into any currently * defined ones. */ eraseKeyframes(sequence, startTime, endTime); for (let i = 0; i < keyframes.length; i++) { sequence.push({ value: keyframes[i], at: motionDom.mixNumber(startTime, endTime, offset[i]), easing: motionUtils.getEasingForSegment(easing, i), }); } } /** * Take an array of times that represent repeated keyframes. For instance * if we have original times of [0, 0.5, 1] then our repeated times will * be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back * down to a 0-1 scale. */ function normalizeTimes(times, repeat) { for (let i = 0; i < times.length; i++) { times[i] = times[i] / (repeat + 1); } } function compareByTime(a, b) { if (a.at === b.at) { if (a.value === null) return 1; if (b.value === null) return -1; return 0; } else { return a.at - b.at; } } const defaultSegmentEasing = "easeInOut"; const MAX_REPEAT = 20; function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) { const defaultDuration = defaultTransition.duration || 0.3; const animationDefinitions = new Map(); const sequences = new Map(); const elementCache = {}; const timeLabels = new Map(); let prevTime = 0; let currentTime = 0; let totalDuration = 0; /** * Build the timeline by mapping over the sequence array and converting * the definitions into keyframes and offsets with absolute time values. * These will later get converted into relative offsets in a second pass. */ for (let i = 0; i < sequence.length; i++) { const segment = sequence[i]; /** * If this is a timeline label, mark it and skip the rest of this iteration. */ if (typeof segment === "string") { timeLabels.set(segment, currentTime); continue; } else if (!Array.isArray(segment)) { timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels)); continue; } let [subject, keyframes, transition = {}] = segment; /** * If a relative or absolute time value has been specified we need to resolve * it in relation to the currentTime. */ if (transition.at !== undefined) { currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels); } /** * Keep track of the maximum duration in this definition. This will be * applied to currentTime once the definition has been parsed. */ let maxDuration = 0; const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => { const valueKeyframesAsList = keyframesAsList(valueKeyframes); const { delay = 0, times = motionDom.defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition; let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition; /** * Resolve stagger() if defined. */ const calculatedDelay = typeof delay === "function" ? delay(elementIndex, numSubjects) : delay; /** * If this animation should and can use a spring, generate a spring easing function. */ const numKeyframes = valueKeyframesAsList.length; const createGenerator = motionDom.isGenerator(type) ? type : generators?.[type || "keyframes"]; if (numKeyframes <= 2 && createGenerator) { /** * As we're creating an easing function from a spring, * ideally we want to generate it using the real distance * between the two keyframes. However this isn't always * possible - in these situations we use 0-100. */ let absoluteDelta = 100; if (numKeyframes === 2 && isNumberKeyframesArray(valueKeyframesAsList)) { const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0]; absoluteDelta = Math.abs(delta); } const springTransition = { ...remainingTransition }; if (duration !== undefined) { springTransition.duration = motionUtils.secondsToMilliseconds(duration); } const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator); ease = springEasing.ease; duration = springEasing.duration; } duration ?? (duration = defaultDuration); const startTime = currentTime + calculatedDelay; /** * If there's only one time offset of 0, fill in a second with length 1 */ if (times.length === 1 && times[0] === 0) { times[1] = 1; } /** * Fill out if offset if fewer offsets than keyframes */ const remainder = times.length - valueKeyframesAsList.length; remainder > 0 && motionDom.fillOffset(times, remainder); /** * If only one value has been set, ie [1], push a null to the start of * the keyframe array. This will let us mark a keyframe at this point * that will later be hydrated with the previous value. */ valueKeyframesAsList.length === 1 && valueKeyframesAsList.unshift(null); /** * Handle repeat options */ if (repeat) { motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20", "repeat-count-high"); duration = calculateRepeatDuration(duration, repeat); const originalKeyframes = [...valueKeyframesAsList]; const originalTimes = [...times]; ease = Array.isArray(ease) ? [...ease] : [ease]; const originalEase = [...ease]; for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) { valueKeyframesAsList.push(...originalKeyframes); for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) { times.push(originalTimes[keyframeIndex] + (repeatIndex + 1)); ease.push(keyframeIndex === 0 ? "linear" : motionUtils.getEasingForSegment(originalEase, keyframeIndex - 1)); } } normalizeTimes(times, repeat); } const targetTime = startTime + duration; /** * Add keyframes, mapping offsets to absolute time. */ addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime); maxDuration = Math.max(calculatedDelay + duration, maxDuration); totalDuration = Math.max(targetTime, totalDuration); }; if (motionDom.isMotionValue(subject)) { const subjectSequence = getSubjectSequence(subject, sequences); resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence)); } else { const subjects = resolveSubjects(subject, keyframes, scope, elementCache); const numSubjects = subjects.length; /** * For every element in this segment, process the defined values. */ for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) { /** * Cast necessary, but we know these are of this type */ keyframes = keyframes; transition = transition; const thisSubject = subjects[subjectIndex]; const subjectSequence = getSubjectSequence(thisSubject, sequences); for (const key in keyframes) { resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects); } } } prevTime = currentTime; currentTime += maxDuration; } /** * For every element and value combination create a new animation. */ sequences.forEach((valueSequences, element) => { for (const key in valueSequences) { const valueSequence = valueSequences[key]; /** * Arrange all the keyframes in ascending time order. */ valueSequence.sort(compareByTime); const keyframes = []; const valueOffset = []; const valueEasing = []; /** * For each keyframe, translate absolute times into * relative offsets based on the total duration of the timeline. */ for (let i = 0; i < valueSequence.length; i++) { const { at, value, easing } = valueSequence[i]; keyframes.push(value); valueOffset.push(motionUtils.progress(0, totalDuration, at)); valueEasing.push(easing || "easeOut"); } /** * If the first keyframe doesn't land on offset: 0 * provide one by duplicating the initial keyframe. This ensures * it snaps to the first keyframe when the animation starts. */ if (valueOffset[0] !== 0) { valueOffset.unshift(0); keyframes.unshift(keyframes[0]); valueEasing.unshift(defaultSegmentEasing); } /** * If the last keyframe doesn't land on offset: 1 * provide one with a null wildcard value. This will ensure it * stays static until the end of the animation. */ if (valueOffset[valueOffset.length - 1] !== 1) { valueOffset.push(1); keyframes.push(null); } if (!animationDefinitions.has(element)) { animationDefinitions.set(element, { keyframes: {}, transition: {}, }); } const definition = animationDefinitions.get(element); definition.keyframes[key] = keyframes; definition.transition[key] = { ...defaultTransition, duration: totalDuration, ease: valueEasing, times: valueOffset, ...sequenceTransition, }; } }); return animationDefinitions; } function getSubjectSequence(subject, sequences) { !sequences.has(subject) && sequences.set(subject, {}); return sequences.get(subject); } function getValueSequence(name, sequences) { if (!sequences[name]) sequences[name] = []; return sequences[name]; } function keyframesAsList(keyframes) { return Array.isArray(keyframes) ? keyframes : [keyframes]; } function getValueTransition(transition, key) { return transition && transition[key] ? { ...transition, ...transition[key], } : { ...transition }; } const isNumber = (keyframe) => typeof keyframe === "number"; const isNumberKeyframesArray = (keyframes) => keyfr