{"version":3,"file":"MeasureLayout.mjs","sources":["../../../../../src/motion/features/layout/MeasureLayout.tsx"],"sourcesContent":["\"use client\"\n\nimport {\n    frame,\n    microtask,\n    globalProjectionState,\n    type IProjectionNode,\n    type VisualElement,\n} from \"motion-dom\"\nimport { Component, useContext } from \"react\"\nimport { usePresence } from \"../../../components/AnimatePresence/use-presence\"\nimport {\n    LayoutGroupContext,\n    LayoutGroupContextProps,\n} from \"../../../context/LayoutGroupContext\"\nimport { SwitchLayoutGroupContext } from \"../../../context/SwitchLayoutGroupContext\"\nimport { MotionProps } from \"../../types\"\n\ninterface MeasureContextProps {\n    layoutGroup: LayoutGroupContextProps\n    switchLayoutGroup?: SwitchLayoutGroupContext\n    isPresent: boolean\n    safeToRemove?: VoidFunction | null\n}\n\ntype MeasureProps = MotionProps &\n    MeasureContextProps & { visualElement: VisualElement }\n\n/**\n * Track whether we've taken any snapshots yet. If not,\n * we can safely skip notification of didUpdate.\n *\n * Difficult to capture in a test but to prevent flickering\n * we must set this to true either on update or unmount.\n * Running `next-env/layout-id` in Safari will show this behaviour if broken.\n */\nlet hasTakenAnySnapshot = false\n\n/**\n * The projection node is created with the props from the component's first\n * render, so keep these in sync as they can change (or be enabled) later.\n */\nconst layoutOptions = ({\n    layout,\n    layoutDependency,\n    layoutAnchor,\n}: MeasureProps) => ({\n    layout,\n    layoutDependency,\n    layoutAnchor,\n    animationType: typeof layout === \"string\" ? layout : \"both\",\n})\n\n/**\n * Move a mounted node to its new layoutId's stack, as an unmount and remount\n * would. Returns a takeover to run once every node in the commit has had the\n * chance to change its layoutId.\n */\nconst switchLayoutId = (projection: IProjectionNode, layoutId?: string) => {\n    const { snapshot } = projection\n    const stack = projection.getStack()\n    stack?.remove(projection)\n    /**\n     * If this node led the old stack (so remove() made it the prevLead of\n     * the new lead), the new lead animates from this node's snapshot, which\n     * it now holds. Unlike a node that unmounts, this one stays mounted, so\n     * unlink them: the new lead mustn't read this node's snapshot after it\n     * changes, or stop or share its animation.\n     */\n    const lead = stack?.lead\n    if (stack?.prevLead === projection && lead!.resumeFrom === projection)\n        lead!.resumeFrom = lead!.currentAnimation = projection.resumingFrom =\n            undefined\n    projection.options.layoutId = layoutId\n\n    /**\n     * A node that followed another no longer does, so clear the styles\n     * that hid it.\n     */\n    if (!layoutId) {\n        projection.needsShow = true\n        return\n    }\n    projection.root!.registerSharedNode(layoutId, projection)\n\n    /**\n     * Like a mounting node, this one animates from the lead it replaces.\n     * But if that lead is still on screen with this layoutId (e.g. an open\n     * card moving to another thumbnail), this node is already on screen\n     * too, so it takes over instantly: it keeps its own snapshot, and the\n     * lead is hidden, as at the end of a crossfade. A lead that left the\n     * stack in this commit (e.g. layoutId={selected ? \"x\" : undefined})\n     * is animated from, as is one that's exiting.\n     */\n    return () => {\n        const { resumeFrom } = projection\n        if (\n            resumeFrom?.isPresent !== false &&\n            projection.getStack()!.members.includes(resumeFrom!)\n        ) {\n            resumeFrom!.hide()\n            projection.resumeFrom = undefined\n            projection.snapshot = snapshot\n        }\n    }\n}\n\nclass MeasureLayoutWithContext extends Component<MeasureProps> {\n    takeOver?: VoidFunction\n\n    /**\n     * This only mounts projection nodes for components that\n     * need measuring, we might want to do it for all components\n     * in order to incorporate transforms\n     */\n    componentDidMount() {\n        const { visualElement, layoutGroup, switchLayoutGroup, layoutId } =\n            this.props\n        const { projection } = visualElement\n\n        if (projection) {\n            layoutGroup.group?.add(projection)\n            layoutId && switchLayoutGroup?.register?.(projection)\n\n            if (hasTakenAnySnapshot) {\n                projection.root!.didUpdate()\n            }\n\n            /**\n             * Without `layout` or `drag`, this only renders while there's a\n             * layoutId, so a mounted node can gain one here.\n             */\n            if (projection.options.layoutId !== layoutId)\n                switchLayoutId(projection, layoutId)?.()\n\n            projection.addEventListener(\"animationComplete\", () =>\n                this.safeToRemove()\n            )\n            projection.setOptions({\n                ...projection.options,\n                ...layoutOptions(this.props),\n                onExitComplete: () => this.safeToRemove(),\n            })\n        }\n\n        globalProjectionState.hasEverUpdated = true\n    }\n\n    getSnapshotBeforeUpdate(prevProps: MeasureProps) {\n        const { layoutDependency, layoutId, visualElement, drag, isPresent } =\n            this.props\n        const { projection } = visualElement\n\n        if (!projection) return null\n\n        /**\n         * TODO: We use this data in relegate to determine whether to\n         * promote a previous element. There's no guarantee its presence data\n         * will have updated by this point - if a bug like this arises it will\n         * have to be that we markForRelegation and then find a new lead some other way,\n         * perhaps in didUpdate\n         */\n        projection.isPresent = isPresent\n\n        this.takeOver =\n            prevProps.layoutId !== layoutId\n                ? (projection.willUpdate(), switchLayoutId(projection, layoutId))\n                : undefined\n\n        projection.setOptions({\n            ...projection.options,\n            ...layoutOptions(this.props),\n        })\n\n        hasTakenAnySnapshot = true\n\n        if (\n            drag ||\n            prevProps.layoutDependency !== layoutDependency ||\n            layoutDependency === undefined ||\n            prevProps.isPresent !== isPresent\n        ) {\n            projection.willUpdate()\n        } else {\n            this.safeToRemove()\n        }\n\n        if (prevProps.isPresent !== isPresent) {\n            if (isPresent) {\n                projection.promote()\n            } else if (!projection.relegate()) {\n                /**\n                 * If there's another stack member taking over from this one,\n                 * it's in charge of the exit animation and therefore should\n                 * be in charge of the safe to remove. Otherwise we call it here.\n                 */\n                frame.postRender(() => {\n                    projection.getStack()?.members.length || this.safeToRemove()\n                })\n            }\n        }\n\n        return null\n    }\n\n    componentDidUpdate() {\n        const { projection } = this.props.visualElement\n        if (projection) {\n            this.takeOver?.()\n            projection.root!.didUpdate()\n\n            microtask.postRender(() => {\n                if (!projection.currentAnimation && projection.isLead()) {\n                    this.safeToRemove()\n                }\n            })\n        }\n    }\n\n    componentWillUnmount() {\n        const {\n            visualElement,\n            layoutGroup,\n            switchLayoutGroup: promoteContext,\n        } = this.props\n        const { projection } = visualElement\n\n        hasTakenAnySnapshot = true\n\n        if (projection) {\n            projection.scheduleCheckAfterUnmount()\n            layoutGroup.group?.remove(projection)\n            promoteContext?.deregister?.(projection)\n        }\n    }\n\n    safeToRemove() {\n        this.props.safeToRemove?.()\n    }\n\n    render() {\n        return null\n    }\n}\n\nexport function MeasureLayout(\n    props: MotionProps & { visualElement: VisualElement }\n) {\n    const [isPresent, safeToRemove] = usePresence()\n    const layoutGroup = useContext(LayoutGroupContext)\n\n    return (\n        <MeasureLayoutWithContext\n            {...props}\n            layoutGroup={layoutGroup}\n            switchLayoutGroup={useContext(SwitchLayoutGroupContext)}\n            isPresent={isPresent}\n            safeToRemove={safeToRemove}\n        />\n    )\n}\n"],"names":[],"mappings":";;;;;;;;AA4BA;;;;;;;AAOG;AACH;AAEA;;;AAGG;AACH;;;;AAQI;AACH;AAED;;;;AAIG;AACH;AACI;AACA;AACA;AACA;;;;;;AAMG;AACH;;;AAGQ;AACR;AAEA;;;AAGG;;AAEC;;;;AAKJ;;;;;;;;AAQG;AACH;AACI;AACA;;;AAKI;AACA;;AAER;AACJ;AAEA;AAGI;;;;AAIG;;AAEC;AAEA;;AAGI;;;AAII;;AAGJ;;;AAGG;AACH;AACI;AAEJ;;;AAKI;AACA;AACH;;AAGL;;AAGJ;AACI;AAEA;AAEA;AAAiB;AAEjB;;;;;;AAMG;AACH;AAEA;;AAEQ;;;;AAKJ;AACH;;AAID;;AAGI;AACA;;;;;;AAOJ;;;;AAGW;AACH;;;;AAIG;AACH;AACI;AACJ;;;AAIR;;;;;AAMI;AACA;AAEA;;;;AAIA;;;;AAKJ;AAKA;;;;AAMI;AACA;;;;AAKJ;;;AAIA;;AAEP;AAEK;;AAIF;;AAWJ;;"}