{"version":3,"file":"Board.mjs","sources":["../../../../src/components/Reorder/Board.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n    elementDragControls,\n    microtask,\n    visualElementStore,\n    type VisualElementDragControls,\n} from \"motion-dom\"\nimport type { Point } from \"motion-utils\"\nimport * as React from \"react\"\nimport { useRef } from \"react\"\nimport { ReorderBoardContext } from \"../../context/ReorderBoardContext\"\nimport { useConstant } from \"../../utils/use-constant\"\nimport { useIsomorphicLayoutEffect } from \"../../utils/use-isomorphic-effect\"\nimport type {\n    ReorderAxis,\n    ReorderBoardContextProps,\n    ReorderContextProps,\n} from \"./types\"\n\nexport interface Props<V> {\n    /**\n     * Whether `value` can move between two groups, from the group whose\n     * values are `from` to the group whose values are `to`. It's called\n     * before either group's `onReorder`. Return `false` to leave the item\n     * where it is: a dragged item stays in its group until it's dragged\n     * over another, and a keyboard move skips on to the next group that\n     * way. Escape always returns a grabbed item to where it was grabbed.\n     *\n     * While a dragged item is over a group it refuses, it's called every\n     * frame, so keep it fast and free of side effects.\n     *\n     * ```jsx\n     * <Reorder.Board canMove={(card, from, to) => to.length < 5}>\n     * ```\n     *\n     * @public\n     */\n    canMove?: (value: V, from: V[], to: V[]) => boolean\n}\n\ntype Group = ReorderContextProps<any>\ntype DragEvent = MouseEvent | TouchEvent | PointerEvent\n\n/**\n * An item moved to another group, to be picked up by its new component.\n */\ninterface Handoff {\n    value: unknown\n    to: Group[\"groupRef\"]\n    /**\n     * The element it was moved from, and its centre in page coordinates.\n     */\n    element: Element\n    centre: Point\n    /**\n     * When dragged: the gesture, and its latest event to continue it from.\n     */\n    controls?: VisualElementDragControls\n    event?: DragEvent\n    /**\n     * When moved with the keyboard: the values to keep it grabbed with, if\n     * it's still grabbed.\n     */\n    keys?: boolean\n    from?: unknown[]\n}\n\nconst centre = (element: Element): Point => {\n    const { left, top, width, height } = element.getBoundingClientRect()\n    return { x: left + width / 2, y: top + height / 2 }\n}\n\n/**\n * Its centre in page coordinates.\n */\nconst pageCentre = (element: Element): Point => {\n    const { x, y } = centre(element)\n    const { scrollX, scrollY } = element.ownerDocument.defaultView!\n    return { x: x + scrollX, y: y + scrollY }\n}\n\n/**\n * Whether `point` comes before `element` in its group's order: before its\n * centre on `axis` or, on \"xy\", above its line or before its centre on it.\n */\nfunction isBefore(\n    point: Point,\n    element: Element,\n    axis: ReorderAxis,\n    isRTL: boolean\n) {\n    const { left, right, top, bottom } = element.getBoundingClientRect()\n    const x = isRTL\n        ? point.x > (left + right) / 2\n        : point.x <= (left + right) / 2\n    const y = point.y <= (top + bottom) / 2\n\n    return axis === \"x\"\n        ? x\n        : axis === \"y\"\n        ? y\n        : point.y < top || (point.y < bottom && x)\n}\n\n/**\n * Whether one group is, or holds, the other: like a list of columns and the\n * list of cards in one of them. Items only move between unrelated groups.\n */\nconst isNested = (a: Group, b: Group) => {\n    const x = a.groupRef.current!\n    const y = b.groupRef.current!\n    return x.contains(y) || y.contains(x)\n}\n\nfunction createBoard(props: {\n    current: Props<unknown>\n}): ReorderBoardContextProps {\n    const groups = new Set<Group>()\n    let handoff: Handoff | undefined\n\n    /**\n     * Whether `value` can move between these groups, if the app lets it.\n     */\n    const canMove = (from: Group, to: Group, value: unknown) =>\n        !isNested(from, to) &&\n        props.current.canMove?.(value, from.values, to.values) !== false\n\n    /**\n     * Where the latest keyboard grab began, for Escape to return it there.\n     */\n    let origin: { group: Group[\"groupRef\"]; from: unknown[] } | undefined\n\n    /**\n     * Move `value` from one group to another: to `index` in its values or,\n     * by default, the slot under the item's centre.\n     */\n    const move = (\n        from: Group,\n        to: Group,\n        value: unknown,\n        element: Element,\n        props: Partial<Handoff>,\n        index?: number\n    ) => {\n        const point = centre(element)\n        const { values } = to\n\n        if (index === undefined) {\n            /**\n             * Only the items it shows: a group keeps unmounted (e.g.\n             * virtualized) items' data while their values remain.\n             */\n            const order = to\n                .getOrder()\n                .filter((item) => item.ref?.current?.isConnected)\n            const isRTL =\n                getComputedStyle(to.groupRef.current!).direction === \"rtl\"\n            const next = order.find((item) =>\n                isBefore(point, item.ref!.current!, to.axis, isRTL)\n            )\n            const last = order[order.length - 1]\n            index = next\n                ? values.indexOf(next.value)\n                : last\n                ? values.indexOf(last.value) + 1\n                : values.length\n        }\n\n        handoff = {\n            value,\n            to: to.groupRef,\n            element,\n            centre: pageCentre(element),\n            ...props,\n        }\n\n        /**\n         * The release that ends a drag lands over the new group.\n         */\n        to.hasReordered.current = true\n\n        from.onReorder(\n            from.values.filter((item) => item !== value),\n            value\n        )\n        to.onReorder(\n            [...values.slice(0, index), value, ...values.slice(index)],\n            value\n        )\n    }\n\n    return {\n        add: (group) => {\n            groups.add(group)\n            return () => groups.delete(group)\n        },\n\n        drag: (from, value, element, event) => {\n            /**\n             * The item can unmount mid-drag (e.g. the app removed it), while\n             * its gesture carries on until release.\n             */\n            if (!element?.isConnected) return false\n\n            /**\n             * Wait for the item to arrive in its new group before moving\n             * it again, keeping up with where it's dragged meanwhile, for\n             * its drag to carry on from there. If the groups don't take\n             * it, it stays here until this gesture ends.\n             */\n            if (handoff?.controls?.isDragging) {\n                if (handoff.value === value) {\n                    handoff.centre = pageCentre(element)\n                    handoff.event = event\n                }\n                return true\n            }\n\n            const point = centre(element)\n            let to: Group | undefined\n\n            for (const hit of element.ownerDocument.elementsFromPoint(\n                point.x,\n                point.y\n            )) {\n                // Skip the item, which is drawn above everything else\n                if (element.contains(hit)) continue\n\n                for (const group of groups) {\n                    if (group.groupRef.current === hit) to = group\n                }\n\n                if (to) break\n            }\n\n            if (!to || !canMove(from, to, value)) return false\n\n            const visualElement = visualElementStore.get(element)\n            move(from, to, value, element, {\n                controls:\n                    visualElement && elementDragControls.get(visualElement),\n                event,\n            })\n\n            return true\n        },\n\n        key: (from, value, keyIndex, element, grabbed) => {\n            const point = centre(element)\n            const axis = keyIndex % 2 ? \"x\" : \"y\"\n            const direction = keyIndex > 1 ? 1 : -1\n            let to: Group | undefined\n            let nearest = Infinity\n\n            for (const group of groups) {\n                const target = group.groupRef.current\n                if (!target) continue\n\n                const { left, right, top, bottom, width, height } =\n                    target.getBoundingClientRect()\n\n                // Hidden, e.g. display: none\n                if (!width && !height) continue\n\n                /**\n                 * Its near edge is past the item that way, so a group beside\n                 * this one isn't below its last item, whatever their heights.\n                 */\n                const edge = [bottom, right, top, left][keyIndex]\n                if ((edge - point[axis]) * direction <= 0) continue\n\n                const x = Math.max(left - point.x, 0, point.x - right)\n                const y = Math.max(top - point.y, 0, point.y - bottom)\n                if (x * x + y * y < nearest && canMove(from, group, value)) {\n                    nearest = x * x + y * y\n                    to = group\n                }\n            }\n\n            if (!to) return false\n\n            if (origin?.from !== grabbed) {\n                origin = { group: from.groupRef, from: grabbed }\n            }\n            move(from, to, value, element, { keys: true, from: grabbed })\n\n            return true\n        },\n\n        restore: (from, value, element, grabbed) => {\n            const to =\n                origin?.from === grabbed &&\n                [...groups].find((group) => group.groupRef === origin!.group)\n\n            if (!to || to.groupRef === from.groupRef) return false\n\n            /**\n             * Before the first value that followed it when it was grabbed,\n             * and is still there.\n             */\n            const after = grabbed.slice(grabbed.indexOf(value) + 1)\n            const index = to.values.findIndex((item) => after.includes(item))\n\n            origin = undefined\n            move(\n                from,\n                to,\n                value,\n                element,\n                { keys: true },\n                index < 0 ? to.values.length : index\n            )\n\n            return true\n        },\n\n        end: (value) => {\n            if (handoff?.value === value) handoff = undefined\n        },\n\n        isMoving: (element) => handoff?.element === element,\n\n        receive: (to, value, element, grab) => {\n            const visualElement = visualElementStore.get(element)\n            const nextControls =\n                visualElement && elementDragControls.get(visualElement)\n\n            /**\n             * React 19's Strict Mode remounts a new item, which resets its\n             * whileDrag, after its drag may already have carried on.\n             */\n            nextControls?.isDragging &&\n                visualElement!.animationState?.setActive(\"whileDrag\", true)\n\n            const latest = handoff\n            if (\n                !latest ||\n                latest.value !== value ||\n                latest.to !== to.groupRef\n            ) {\n                return false\n            }\n\n            const { keys, from, controls } = latest\n            /**\n             * Whether the groups moved the item, rather than copied it.\n             */\n            const isMoved = !latest.element.isConnected\n\n            if (keys) {\n                handoff = undefined\n                /**\n                 * Unless focus has moved on since, e.g. the groups took it\n                 * much later.\n                 */\n                const { activeElement, body } = element.ownerDocument\n                if (isMoved && (!activeElement || activeElement === body)) {\n                    ;(element as HTMLElement).focus()\n                    grab(element, from)\n                }\n                return false\n            }\n\n            if (!nextControls || !controls?.isDragging) return false\n\n            /**\n             * Once the commit that moved the item has been measured, and\n             * before it's drawn. Until then, the item's drag in its\n             * previous group waits.\n             */\n            microtask.read(() => {\n                if (handoff !== latest) return\n                handoff = undefined\n\n                /**\n                 * Start it where, and as, the item was dragged to (e.g.\n                 * mid-way through its whileDrag animation), and carry on\n                 * dragging it from there. Values are read from the item\n                 * first, for its whileDrag to return to on release.\n                 */\n                visualElementStore\n                    .get(latest.element)\n                    ?.values.forEach((motionValue, key) => {\n                        const current = motionValue.get()\n                        visualElement!\n                            .getValue(\n                                key,\n                                visualElement!.readValue(key, current)\n                            )\n                            ?.set(current)\n                    })\n\n                /**\n                 * Into the item's own coordinates, e.g. in a scaled parent.\n                 */\n                const transform =\n                    visualElement!.getTransformPagePoint() || ((p: Point) => p)\n                const a = transform(latest.centre)\n                const b = transform(pageCentre(element))\n                visualElement!.getValue(\"x\")!.set(a.x - b.x)\n                visualElement!.getValue(\"y\")!.set(a.y - b.y)\n                visualElement!.render()\n\n                /**\n                 * If the item is still in its previous group, animate it\n                 * back to its slot there.\n                 */\n                isMoved ? controls.cancel() : controls.stop()\n                nextControls.start(latest.event as PointerEvent, {\n                    immediate: true,\n                })\n            })\n\n            /**\n             * Its drag already started in its previous group.\n             */\n            return isMoved\n        },\n\n        column: ({ groupRef }) => {\n            let element = groupRef.current\n            const holdsOther = (ancestor: Element) =>\n                [...groups].some(\n                    (group) =>\n                        group.groupRef !== groupRef &&\n                        ancestor.contains(group.groupRef.current)\n                )\n\n            while (\n                element?.parentElement?.parentElement &&\n                !holdsOther(element.parentElement)\n            ) {\n                element = element.parentElement\n            }\n\n            return element\n        },\n    }\n}\n\n/**\n * Lets items be dragged between the `Reorder.Group`s inside it.\n *\n * ```jsx\n * <Reorder.Board>\n *   <Reorder.Group values={todo} onReorder={setTodo}>\n *     {todo.map((item) => <Reorder.Item key={item} value={item} />)}\n *   </Reorder.Group>\n *   <Reorder.Group values={done} onReorder={setDone}>\n *     {done.map((item) => <Reorder.Item key={item} value={item} />)}\n *   </Reorder.Group>\n * </Reorder.Board>\n * ```\n *\n * When an item's centre is dragged over another group, it moves there:\n * the group it left calls `onReorder` without it, and the group it joined\n * calls `onReorder` with it, both passing it as the moved value. Their\n * other items animate to make room, and the drag carries on in the new\n * group.\n *\n * Grabbed with the keyboard, an item moves to the nearest group in the\n * direction of an arrow key across its group's axis, or along it from\n * either end. Escape returns it to the group and slot it was grabbed from.\n *\n * `canMove` can refuse a move before either group updates.\n *\n * Values must be unique across the groups. If the app keeps the groups'\n * values in one state, update it from the latest state (e.g.\n * `setColumns((columns) => ...)`), as both groups update it in turn.\n *\n * @public\n */\nexport function ReorderBoard<V = any>({\n    children,\n    ...props\n}: React.PropsWithChildren<Props<V>>) {\n    const latest = useRef(props as Props<unknown>)\n\n    /**\n     * In a layout effect, so a render React discards can't leave its\n     * props here.\n     */\n    useIsomorphicLayoutEffect(() => {\n        latest.current = props as Props<unknown>\n    })\n\n    return (\n        <ReorderBoardContext.Provider\n            value={useConstant(() => createBoard(latest))}\n        >\n            {children}\n        </ReorderBoardContext.Provider>\n    )\n}\n"],"names":[],"mappings":";;;;;;;;AAoEA;AACI;AACA;AACJ;AAEA;;AAEG;AACH;;;AAGI;AACJ;AAEA;;;AAGG;AACH;AAMI;;;AAGI;AACJ;;AAGI;;AAEA;AACA;AACR;AAEA;;;AAGG;AACH;AACI;AACA;AACA;AACJ;AAEA;AAGI;AACA;AAEA;;AAEG;AACH;AAEI;AAEJ;;AAEG;AACH;AAEA;;;AAGG;AACH;AAQI;AACA;AAEA;AACI;;;AAGG;;AAEE;AACA;AACL;AAEA;;AAIA;;AAEI;;AAEA;;AAGR;;;;AAII;AACA;;AAGJ;;AAEG;AACH;;;AAUJ;;AAGI;AACI;;;;AAKA;;;AAGG;;AACwB;AAE3B;;;;;AAKG;AACH;AACI;AACI;AACA;;AAEJ;;AAGJ;AACA;AAEA;;AAKI;;AAEA;AACI;;;AAGJ;;;;AAGkC;;;;;AAOrC;AAED;;AAGJ;AACI;AACA;AACA;AACA;;AAGA;AACI;AACA;;AAEA;;AAIA;;AAEA;;;AAGG;AACH;;;;;AAKA;;;;;AAMJ;AAAS;AAET;AACI;;AAEJ;AAEA;;;AAIA;AAEI;;AAEsC;AAE1C;;;AAGG;AACH;;;AAIA;AASA;;AAGJ;AACI;;;;;;;AAUA;;;AAGG;AACH;;;AAIA;;AAGI;AAEA;;;AAIJ;;AAEG;;;;AAKC;;;AAGG;;;;AAIC;;AAEJ;;AAGJ;AAA4C;AAE5C;;;;AAIG;AACH;;;;AAII;;;;;AAKG;;AAEE;;AAEG;;;AAMI;AACR;AAEJ;;AAEG;AACH;;;AAIA;AACA;;AAGA;;;AAGG;AACH;AACA;AACI;AACH;AACL;AAEA;;AAEG;AACH;;AAGJ;AACI;;;AAQA;AAEI;AAEA;;AAGJ;;;AAGZ;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BG;AACG;AAIF;AAEA;;;AAGG;;AAEC;AACJ;;AASJ;;"}