diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewBody.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewBody.tsx
new file mode 100644
index 000000000000..74adda4aa0d7
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewBody.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native';
+import {getButtonRole} from '@components/Button/utils';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import ControlSelection from '@libs/ControlSelection';
+import {canUseTouchScreen} from '@libs/DeviceCapabilities';
+import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import {useReportPreviewActions, useReportPreviewAnimationState, useReportPreviewData, useReportPreviewMeta, useReportPreviewUIState} from './MoneyRequestReportPreviewContext';
+import ReportPreviewActionButton from './ReportPreviewActionButton';
+import ReportPreviewHeader from './ReportPreviewHeader';
+import ReportPreviewHoldMenu from './ReportPreviewHoldMenu';
+import ReportPreviewTotal from './ReportPreviewTotal';
+import TransactionReportCarousel from './TransactionReportCarousel';
+
+type MoneyRequestReportPreviewBodyProps = ForwardedFSClassProps & {
+    /** Callback passed to the wrapper view's onLayout */
+    onWrapperLayout: (e: LayoutChangeEvent) => void;
+
+    /** Callback passed to the carousel-width-setter view's onLayout */
+    onCarouselLayout: (e: LayoutChangeEvent) => void;
+
+    /** Extra styles to pass to the carousel-width-setter view */
+    containerStyles?: StyleProp<ViewStyle>;
+
+    /** Called when the whole preview is pressed */
+    onPress: () => void;
+
+    /** Whether the corresponding report action item is hovered */
+    isHovered?: boolean;
+
+    /** Whether a message is a whisper */
+    isWhisper?: boolean;
+
+    /** Whether to show a border separating the chat item and the preview */
+    shouldShowBorder?: boolean;
+};
+
+/**
+ * The layout body of the money request report preview: the pressable card that composes the header, carousel, action
+ * button + total, and the hold menu. It owns no data — everything comes from the context slices via `useX()` hooks,
+ * except the pure layout wiring passed as props.
+ */
+function MoneyRequestReportPreviewBody({
+    onWrapperLayout,
+    onCarouselLayout,
+    containerStyles,
+    onPress,
+    isHovered = false,
+    isWhisper = false,
+    shouldShowBorder = false,
+    forwardedFSClass,
+}: MoneyRequestReportPreviewBodyProps) {
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const StyleUtils = useStyleUtils();
+    const {translate} = useLocalize();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+    const {anchor: contextMenuAnchorRef, shouldDisplayContextMenu = true, originalReportID} = useShowContextMenuState();
+    const {checkIfContextMenuActive} = useShowContextMenuActions();
+
+    const {iouReportID, chatReportID, action, iouReport, chatReport} = useReportPreviewData();
+    const {isTransitionPending, isScanning, reportPreviewStyles} = useReportPreviewUIState();
+    const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning} = useReportPreviewAnimationState();
+    const {openReportFromPreview, onHoldMenuOpen, onPaymentOptionsShow, onPaymentOptionsHide, stopAnimation, startAnimation, startApprovedAnimation, startSubmittingAnimation} =
+        useReportPreviewActions();
+    const {holdMenuRef} = useReportPreviewMeta();
+
+    const isReportDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
+    const totalAmountStyle = shouldUseNarrowLayout ? [styles.flexColumnReverse, styles.alignItemsStretch] : [styles.flexRow, styles.alignItemsCenter];
+
+    return (
+        <View
+            onLayout={onWrapperLayout}
+            testID="MoneyRequestReportPreviewContent-wrapper"
+            fsClass={forwardedFSClass}
+        >
+            <OfflineWithFeedback
+                pendingAction={iouReport?.pendingFields?.preview}
+                shouldDisableOpacity={!!(action.pendingAction ?? action.isOptimisticAction)}
+                needsOffscreenAlphaCompositing
+                style={styles.mt1}
+            >
+                <View
+                    style={[styles.chatItemMessage, isReportDeleted && [styles.cursorDisabled, styles.pointerEventsAuto], containerStyles, isTransitionPending && styles.w100]}
+                    onLayout={onCarouselLayout}
+                    testID="carouselWidthSetter"
+                >
+                    <PressableWithoutFeedback
+                        onPress={onPress}
+                        onPressIn={() => canUseTouchScreen() && ControlSelection.block()}
+                        onPressOut={() => ControlSelection.unblock()}
+                        onLongPress={(event) => {
+                            if (!shouldDisplayContextMenu) {
+                                return;
+                            }
+                            showContextMenuForReport(event, contextMenuAnchorRef, chatReportID, action, checkIfContextMenuActive, originalReportID);
+                        }}
+                        shouldUseHapticsOnLongPress
+                        style={[
+                            styles.flexRow,
+                            styles.justifyContentBetween,
+                            StyleUtils.getBackgroundColorStyle(theme.cardBG),
+                            shouldShowBorder ? styles.borderedContentCardLarge : styles.reportContainerBorderRadius,
+                            isReportDeleted && styles.pointerEventsNone,
+                        ]}
+                        role={getButtonRole(true)}
+                        isNested
+                        accessibilityLabel={translate('iou.viewDetails')}
+                        sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CARD}
+                    >
+                        <View
+                            style={[
+                                StyleUtils.getBackgroundColorStyle(theme.cardBG),
+                                styles.reportContainerBorderRadius,
+                                styles.w100,
+                                (isHovered || isScanning || isWhisper) && styles.reportPreviewBoxHoverBorder,
+                            ]}
+                        >
+                            <View style={[reportPreviewStyles.wrapperStyle]}>
+                                <View style={[reportPreviewStyles.contentContainerStyle, styles.gap4]}>
+                                    <ReportPreviewHeader />
+                                    <TransactionReportCarousel />
+                                    <View style={[styles.expenseAndReportPreviewTextContainer]}>
+                                        <View style={[totalAmountStyle, styles.justifyContentBetween, styles.gap4, StyleUtils.getMinimumHeight(variables.h28)]}>
+                                            <ReportPreviewActionButton
+                                                iouReportID={iouReportID}
+                                                chatReportID={chatReportID}
+                                                chatReport={chatReport}
+                                                iouReport={iouReport}
+                                                isPaidAnimationRunning={isPaidAnimationRunning}
+                                                isApprovedAnimationRunning={isApprovedAnimationRunning}
+                                                isSubmittingAnimationRunning={isSubmittingAnimationRunning}
+                                                stopAnimation={stopAnimation}
+                                                startAnimation={startAnimation}
+                                                startApprovedAnimation={startApprovedAnimation}
+                                                startSubmittingAnimation={startSubmittingAnimation}
+                                                onPaymentOptionsShow={onPaymentOptionsShow}
+                                                onPaymentOptionsHide={onPaymentOptionsHide}
+                                                openReportFromPreview={openReportFromPreview}
+                                                onHoldMenuOpen={onHoldMenuOpen}
+                                                transactionPreviewCarouselWidth={reportPreviewStyles.transactionPreviewCarouselStyle.width}
+                                            />
+                                            <ReportPreviewTotal />
+                                        </View>
+                                    </View>
+                                </View>
+                            </View>
+                        </View>
+                    </PressableWithoutFeedback>
+                </View>
+                <ReportPreviewHoldMenu ref={holdMenuRef} />
+            </OfflineWithFeedback>
+        </View>
+    );
+}
+
+export default MoneyRequestReportPreviewBody;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
index 0c002857292b..01b6e305fc8d 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
@@ -1,85 +1,13 @@
-import {useFocusEffect} from '@react-navigation/native';
-import {FlashList} from '@shopify/flash-list';
-import type {FlashListRef, ListRenderItemInfo} from '@shopify/flash-list';
-import React, {useCallback, useDeferredValue, useEffect, useMemo, useRef, useState} from 'react';
-import {View} from 'react-native';
-import type {ViewToken} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated';
-import ActivityIndicator from '@components/ActivityIndicator';
-import {getButtonRole} from '@components/Button/utils';
-import Icon from '@components/Icon';
-import MoneyReportHeaderStatusBarSkeleton from '@components/MoneyReportHeaderStatusBarSkeleton';
-import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {PressableWithFeedback} from '@components/Pressable';
-import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
-import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
-import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
-import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext';
-import StatusBadge from '@components/StatusBadge';
-import Text from '@components/Text';
-import {useCurrencyListActions} from '@hooks/useCurrencyList';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
-import useIsFocusedRef from '@hooks/useIsFocusedRef';
-import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
-import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
-import usePaymentAnimations from '@hooks/usePaymentAnimations';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
-import ControlSelection from '@libs/ControlSelection';
-import {canUseTouchScreen} from '@libs/DeviceCapabilities';
-import Navigation from '@libs/Navigation/Navigation';
-import TransitionTracker from '@libs/Navigation/TransitionTracker';
-import {getInvoicePayerName, getReportName} from '@libs/ReportNameUtils';
-import {
-    areAllRequestsBeingSmartScanned as areAllRequestsBeingSmartScannedReportUtils,
-    getDisplayNameForParticipant,
-    getMoneyRequestSpendBreakdown,
-    getNonHeldAndFullAmount,
-    getPolicyName,
-    getReportStatusColorStyle,
-    getReportStatusTranslation,
-    getTransactionsWithReceipts,
-    hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils,
-    hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils,
-    hasOnlyTransactionsWithPendingRoutes as hasOnlyTransactionsWithPendingRoutesReportUtils,
-    isInvoiceRoom as isInvoiceRoomReportUtils,
-    isPolicyExpenseChat as isPolicyExpenseChatReportUtils,
-    isReportApproved,
-    isSettled,
-    isTripRoom as isTripRoomReportUtils,
-} from '@libs/ReportUtils';
-import shouldAdjustScroll from '@libs/shouldAdjustScroll';
-import {startSpan} from '@libs/telemetry/activeSpans';
-import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
-import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
-import {compareByRBR} from '@libs/TransactionPreviewUtils';
-import {getCreated, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
-import colors from '@styles/theme/colors';
-import variables from '@styles/variables';
-import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import type {ReportAttributesDerivedValue, Transaction} from '@src/types/onyx';
-import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
-import AccessMoneyRequestReportPreviewPlaceHolder from './AccessMoneyRequestReportPreviewPlaceHolder';
-import EmptyMoneyRequestReportPreview from './EmptyMoneyRequestReportPreview';
-import ReportPreviewActionButton from './ReportPreviewActionButton';
+import React from 'react';
+import MoneyRequestReportPreviewBody from './MoneyRequestReportPreviewBody';
+import MoneyRequestReportPreviewProvider from './MoneyRequestReportPreviewProvider';
 import type {MoneyRequestReportPreviewContentProps} from './types';
 
-const MAX_PREVIEWS_NUMBER = 10;
-
-const ITEM_LAYOUT_TYPE = {
-    PREVIEW: 'preview',
-    SHOW_MORE: 'showMore',
-};
-
-const reportAttributesSelector = (c: OnyxEntry<ReportAttributesDerivedValue>) => c?.reports;
-
+/**
+ * Entry point for the money request report preview. Sets up the context provider (which owns all state/derivations)
+ * around the layout body, mirroring how `ReportActionCompose` wraps its layout in `ComposerProvider`. Keeps the same
+ * prop contract so `index.tsx` and the storybook story are unaffected.
+ */
 function MoneyRequestReportPreviewContent({
     iouReportID,
     newTransactionIDs,
@@ -106,706 +34,36 @@ function MoneyRequestReportPreviewContent({
     onPress,
     forwardedFSClass,
 }: MoneyRequestReportPreviewContentProps) {
-    const {anchor: contextMenuAnchorRef, shouldDisplayContextMenu = true, originalReportID} = useShowContextMenuState();
-    const {checkIfContextMenuActive} = useShowContextMenuActions();
-    const [chatReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`);
-    const [chatReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`);
-
-    const [isTransitionPending, setIsTransitionPending] = useState(() => {
-        const pending = getPendingSubmitFollowUpAction();
-        return pending?.followUpAction === CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT && (pending?.reportID === chatReportID || pending?.reportID === iouReportID);
-    });
-
-    useFocusEffect(
-        useCallback(() => {
-            if (!isTransitionPending) {
-                return;
-            }
-            const handle = TransitionTracker.runAfterTransitions({
-                callback: () => setIsTransitionPending(false),
-                waitForUpcomingTransition: true,
-            });
-            return () => handle.cancel();
-        }, [isTransitionPending]),
-    );
-
-    const shouldShowLoading =
-        chatReportLoadingState != null && chatReportLoadingState.hasOnceLoadedReportActions !== true && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
-    // `hasOnceLoadedReportActions` becomes true before transactions populate fully,
-    const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
-    // so we defer the loading state update to ensure transactions are loaded
-    const shouldShowLoadingDeferred = useDeferredValue(shouldShowLoading);
-    const lastTransaction = transactions?.at(0);
-    const shouldShowSkeleton = shouldShowLoading && transactions.length === 0;
-    const shouldShowAccessPlaceHolder = !iouReport && !shouldShowLoading;
-    const shouldShowEmptyPlaceholder = transactions.length === 0 && !shouldShowLoading;
-    const shouldShowPreviewPlaceholder = shouldShowAccessPlaceHolder || shouldShowEmptyPlaceholder;
-    const showStatusAndSkeleton = !shouldShowEmptyPlaceholder;
-    // Empty/access placeholders do not depend on measured carousel width, so we can show them immediately
-    // once the report data is ready instead of keeping the taller loading state around and causing the preview to reflow.
-    const shouldShowPreviewLoading = isTransitionPending || shouldShowLoading || shouldShowLoadingDeferred || (!currentWidth && !shouldShowPreviewPlaceholder);
-    const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
-        context: 'MoneyRequestReportPreviewContent',
-        hasOnceLoadedReportActions: chatReportLoadingState?.hasOnceLoadedReportActions,
-        isTransactionsEmpty: transactions.length === 0,
-        isOptimisticReport: chatReportMetadata?.isOptimisticReport,
-    };
-    const carouselReasonAttributes: SkeletonSpanReasonAttributes = {
-        context: 'MoneyRequestReportPreviewContent.Carousel',
-        hasCurrentWidth: !!currentWidth,
-        shouldShowLoading,
-        shouldShowLoadingDeferred,
-    };
-    const theme = useTheme();
-    const styles = useThemeStyles();
-    const StyleUtils = useStyleUtils();
-    const currentUserDetails = useCurrentUserPersonalDetails();
-    const {translate, formatPhoneNumber, localeCompare} = useLocalize();
-    const {convertToDisplayString} = useCurrencyListActions();
-    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
-    const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
-    const previewCarouselMinWidth = shouldUseNarrowLayout ? CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.MIN_NARROW_WIDTH : CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.MIN_WIDE_WIDTH;
-    const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'BackArrow']);
-
-    const {areAllRequestsBeingSmartScanned, hasNonReimbursableTransactions} = useMemo(
-        () => ({
-            areAllRequestsBeingSmartScanned: areAllRequestsBeingSmartScannedReportUtils(iouReportID, action),
-            hasOnlyTransactionsWithPendingRoutes: hasOnlyTransactionsWithPendingRoutesReportUtils(iouReportID),
-            hasNonReimbursableTransactions: hasNonReimbursableTransactionsReportUtils(iouReportID),
-        }),
-        // When transactions get updated these values may have changed, so that is a case where we also want to recompute them
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-        [transactions, iouReportID, action],
-    );
-
-    const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation, startSubmittingAnimation} =
-        usePaymentAnimations();
-
-    const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
-    const [requestType, setRequestType] = useState<ActionHandledType>();
-    const [paymentType, setPaymentType] = useState<PaymentMethodType>();
-    const [methodID, setMethodID] = useState<number>();
-    const [shouldShowPayButton, setShouldShowPayButton] = useState(false);
-    const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(transactions);
-
-    const handleHoldMenuOpen = (holdRequestType: string, holdPaymentType?: PaymentMethodType, canPay?: boolean, holdMethodID?: number) => {
-        setRequestType(holdRequestType as ActionHandledType);
-        setPaymentType(holdPaymentType);
-        setMethodID(holdMethodID);
-        setShouldShowPayButton(!!canPay);
-        setIsHoldMenuVisible(true);
-    };
-
-    const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
-    const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(iouReport);
-
-    const iouSettled = isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
-    const previewMessageOpacity = useSharedValue(1);
-    const previewMessageStyle = useAnimatedStyle(() => ({
-        opacity: previewMessageOpacity.get(),
-    }));
-    const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
-
-    const isApproved = isReportApproved({
-        report: iouReport,
-        parentReportAction: action,
-    });
-    const thumbsUpScale = useSharedValue(isApproved ? 1 : 0);
-
-    const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport);
-    const isInvoiceRoom = isInvoiceRoomReportUtils(chatReport);
-    const isTripRoom = isTripRoomReportUtils(chatReport);
-
-    const numberOfRequests = transactions?.length ?? 0;
-    const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
-    const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => isPending(transaction) && isManagedCardTransaction(transaction)).length;
-
-    const shouldShowRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, lastTransactionViolations);
-
-    const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {
-        selector: reportAttributesSelector,
-    });
-
-    const hasReceipts = transactionsWithReceipts.length > 0;
-    const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
-
-    const previewMessage = useMemo(() => {
-        if (isScanning) {
-            return totalDisplaySpend ? `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('common.scanning')}` : `${translate('common.receipt')}`;
-        }
-        if (numberOfPendingRequests === 1 && numberOfRequests === 1) {
-            return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`;
-        }
-        if (shouldShowRTERViolationMessage) {
-            return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`;
-        }
-
-        let payerOrApproverName;
-        if (isPolicyExpenseChat || isTripRoom) {
-            payerOrApproverName = getPolicyName({report: chatReport, policy});
-        } else if (isInvoiceRoom) {
-            payerOrApproverName = getInvoicePayerName(chatReport, invoiceReceiverPolicy, invoiceReceiverPersonalDetail);
-        } else {
-            payerOrApproverName = getDisplayNameForParticipant({
-                accountID: managerID,
-                shouldUseShortForm: true,
-                formatPhoneNumber,
-            });
-        }
-
-        if (isApproved) {
-            return translate('iou.managerApproved', payerOrApproverName);
-        }
-        let paymentVerb: TranslationPaths = 'iou.payerOwes';
-        if (iouSettled || iouReport?.isWaitingOnBankAccount) {
-            paymentVerb = 'iou.payerPaid';
-        } else if (hasNonReimbursableTransactions) {
-            paymentVerb = 'iou.payerSpent';
-            payerOrApproverName = getDisplayNameForParticipant({
-                accountID: chatReport?.ownerAccountID,
-                shouldUseShortForm: true,
-                formatPhoneNumber,
-            });
-        }
-        return translate(paymentVerb, payerOrApproverName);
-    }, [
-        isScanning,
-        numberOfPendingRequests,
-        numberOfRequests,
-        shouldShowRTERViolationMessage,
-        isPolicyExpenseChat,
-        isTripRoom,
-        isInvoiceRoom,
-        isApproved,
-        iouSettled,
-        iouReport?.isWaitingOnBankAccount,
-        hasNonReimbursableTransactions,
-        translate,
-        totalDisplaySpend,
-        chatReport,
-        policy,
-        invoiceReceiverPolicy,
-        invoiceReceiverPersonalDetail,
-        managerID,
-        formatPhoneNumber,
-    ]);
-
-    /*
-     Show subtitle if at least one of the expenses is not being smart scanned, and either:
-     - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown;
-     - There is only one expense, it has a receipt and is not being smart scanned – in this case, the expense merchant or description is shown;
-
-     * There is an edge case when there is only one distance expense with a pending route and amount = 0.
-       In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field.
-     */
-    const expenseCount = useMemo(
-        () =>
-            translate('iou.expenseCount', {
-                count: numberOfRequests,
-            }),
-        [translate, numberOfRequests],
-    );
-
-    const reportStatus = useMemo(
-        () =>
-            getReportStatusTranslation({
-                stateNum: iouReport?.stateNum ?? action?.childStateNum,
-                statusNum: iouReport?.statusNum ?? action?.childStatusNum,
-                translate,
-            }),
-        [action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, translate],
-    );
-
-    const shouldShowReportStatus = !!reportStatus && !!expenseCount;
-
-    const reportStatusColorStyle = useMemo(
-        () => getReportStatusColorStyle(theme, iouReport?.stateNum ?? action?.childStateNum, iouReport?.statusNum ?? action?.childStatusNum),
-        [action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, theme],
-    );
-
-    const totalAmountStyle = shouldUseNarrowLayout ? [styles.flexColumnReverse, styles.alignItemsStretch] : [styles.flexRow, styles.alignItemsCenter];
-
-    useEffect(() => {
-        if (!isPaidAnimationRunning || isApprovedAnimationRunning || isSubmittingAnimationRunning) {
-            return;
-        }
-
-        previewMessageOpacity.set(
-            withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
-                previewMessageOpacity.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}));
-            }),
-        );
-        // We only want to animate the text when the text changes
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [previewMessage, previewMessageOpacity]);
-
-    useEffect(() => {
-        if (!iouSettled) {
-            return;
-        }
-
-        checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1);
-    }, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
-
-    useEffect(() => {
-        if (!isApproved) {
-            return;
-        }
-
-        thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBS_UP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBS_UP_DURATION})) : 1);
-    }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
-
-    const carouselTransactions = useMemo(() => {
-        if (shouldShowAccessPlaceHolder) {
-            return [];
-        }
-        const sorted = [...transactions].sort((a, b) => {
-            const rbrComparison = compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy);
-            if (rbrComparison !== 0) {
-                return rbrComparison;
-            }
-            // Tiebreak by date (ascending — oldest first) so position is stable across RBR state changes
-            return localeCompare(getCreated(a), getCreated(b));
-        });
-        return sorted.slice(0, 11);
-    }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy, localeCompare]);
-    const prevCarouselTransactionLength = useRef(0);
-
-    useEffect(() => {
-        return () => {
-            prevCarouselTransactionLength.current = carouselTransactions.length;
-        };
-    }, [carouselTransactions.length]);
-
-    const [currentIndex, setCurrentIndex] = useState(0);
-    const [currentVisibleItems, setCurrentVisibleItems] = useState([0]);
-    const [footerWidth, setFooterWidth] = useState(0);
-    // optimisticIndex - value for index we are scrolling to with an arrow button or undefined after scroll is completed
-    // value ensures that disabled state is applied instantly and not overridden by onViewableItemsChanged when scrolling
-    // undefined makes arrow buttons react on currentIndex changes when scrolling manually
-    const [optimisticIndex, setOptimisticIndex] = useState<number | undefined>(undefined);
-    const carouselRef = useRef<FlashListRef<Transaction> | null>(null);
-    const prevTransactionCountForScroll = useRef(carouselTransactions.length);
-    const [carouselKey, setCarouselKey] = useState(0);
-
-    // Reset carousel when transitioning from empty to non-empty data.
-    // scrollToOffset doesn't clear RecyclerListView's internal layout cache on iOS mobile web,
-    // so we force a full re-mount via key to prevent new items from rendering off-screen.
-    useEffect(() => {
-        if (carouselTransactions.length > 0 && prevTransactionCountForScroll.current === 0) {
-            setCurrentIndex(0);
-            setOptimisticIndex(undefined);
-            setCarouselKey((prev) => prev + 1);
-        }
-        prevTransactionCountForScroll.current = carouselTransactions.length;
-    }, [carouselTransactions.length]);
-
-    const visibleItemsOnEndCount = useMemo(() => {
-        const lastItemWidth = transactions.length > MAX_PREVIEWS_NUMBER ? footerWidth : reportPreviewStyles.transactionPreviewCarouselStyle.width;
-        const lastItemWithGap = lastItemWidth + styles.gap2.gap;
-        const itemWithGap = reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.gap2.gap;
-        return Math.floor((currentWidth - 2 * styles.pl2.paddingLeft - lastItemWithGap) / itemWithGap) + 1;
-    }, [transactions.length, footerWidth, reportPreviewStyles.transactionPreviewCarouselStyle.width, styles.gap2.gap, styles.pl2.paddingLeft, currentWidth]);
-    const viewabilityConfig = useMemo(() => {
-        return {itemVisiblePercentThreshold: 100};
-    }, []);
-
-    const carouselTransactionsRef = useRef(carouselTransactions);
-
-    useEffect(() => {
-        carouselTransactionsRef.current = carouselTransactions;
-    }, [carouselTransactions]);
-
-    const isFocusedRef = useIsFocusedRef();
-
-    useEffect(() => {
-        const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.has(transaction.transactionID));
-
-        if (index < 0) {
-            return;
-        }
-        const newTransaction = carouselTransactions.at(index);
-        setTimeout(() => {
-            if (!isFocusedRef.current) {
-                return;
-            }
-
-            // If the new transaction is not available at the index it was on before the delay, avoid the scrolling
-            // because we are scrolling to either a wrong or unavailable transaction (which can cause crash).
-            if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) {
-                return;
-            }
-
-            carouselRef.current?.scrollToIndex({
-                index,
-                viewOffset: -2 * styles.gap2.gap,
-                animated: true,
-            });
-        }, CONST.PENDING_TRANSACTION_SCROLL_DELAY);
-
-        // We only want to scroll to a new transaction when the set of new transaction IDs changes.
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [newTransactionIDs]);
-
-    const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
-        const newIndex = viewableItems.at(0)?.index;
-        if (typeof newIndex === 'number') {
-            setCurrentIndex(newIndex);
-        }
-        const viewableItemsIndexes = viewableItems.map((item) => item.index).filter((item): item is number => item !== null);
-        setCurrentVisibleItems(viewableItemsIndexes);
-    }).current;
-
-    const snapOffsets = carouselTransactions.map((_, index) => index * (reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.transactionsCarouselGap.width));
-
-    const handleChange = (index: number) => {
-        if (index > carouselTransactions.length - visibleItemsOnEndCount) {
-            const lastScrollableIndex = carouselTransactions.length - visibleItemsOnEndCount;
-            setOptimisticIndex(lastScrollableIndex);
-            carouselRef.current?.scrollToOffset({
-                offset: snapOffsets.at(lastScrollableIndex) ?? 0,
-                animated: true,
-            });
-            return;
-        }
-        if (index < 0) {
-            setOptimisticIndex(0);
-            carouselRef.current?.scrollToTop({animated: true});
-            return;
-        }
-        if (index === carouselTransactions.length - visibleItemsOnEndCount) {
-            setOptimisticIndex(index);
-            carouselRef.current?.scrollToEnd({animated: true});
-            return;
-        }
-        setOptimisticIndex(index);
-        carouselRef.current?.scrollToOffset({
-            offset: snapOffsets.at(index) ?? 0,
-            animated: true,
-        });
-    };
-
-    const renderItem = (itemInfo: ListRenderItemInfo<Transaction>) => {
-        if (itemInfo.index > MAX_PREVIEWS_NUMBER - 1) {
-            return (
-                <View
-                    style={[styles.p5, styles.justifyContentCenter]}
-                    onLayout={(e) => setFooterWidth(e.nativeEvent.layout.width)}
-                >
-                    <Text style={{color: colors.blue600}}>
-                        +{transactions.length - MAX_PREVIEWS_NUMBER} {translate('common.more').toLowerCase()}
-                    </Text>
-                </View>
-            );
-        }
-        return renderTransactionItem(itemInfo);
-    };
-
-    useEffect(() => {
-        if (
-            optimisticIndex === undefined ||
-            optimisticIndex !== currentIndex ||
-            // currentIndex is still the same as target (f.ex. 0), but not yet scrolled to the far left
-            (currentVisibleItems.at(0) !== optimisticIndex && optimisticIndex !== undefined) ||
-            // currentIndex reached, but not scrolled to the end
-            (optimisticIndex === carouselTransactions.length - visibleItemsOnEndCount && currentVisibleItems.length !== visibleItemsOnEndCount)
-        ) {
-            return;
-        }
-        setOptimisticIndex(undefined);
-    }, [carouselTransactions.length, currentIndex, currentVisibleItems, currentVisibleItems.length, optimisticIndex, visibleItemsOnEndCount]);
-
-    const openReportFromPreview = useCallback(() => {
-        if (!iouReportID) {
-            return;
-        }
-        startSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${iouReportID}`, {
-            name: 'MoneyRequestReportPreviewContent',
-            op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
-        });
-        // Small screens navigate to full report view since super wide RHP
-        // is not available on narrow layouts and would break the navigation logic.
-        if (isSmallScreenWidth) {
-            Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
-        } else {
-            Navigation.navigate(
-                ROUTES.EXPENSE_REPORT_RHP.getRoute({
-                    reportID: iouReportID,
-                    backTo: Navigation.getActiveRoute(),
-                }),
-            );
-        }
-    }, [iouReportID, isSmallScreenWidth]);
-
-    const isReportDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
-
-    const adjustScroll = useCallback(() => {
-        // Workaround for a known React Native bug on Android (https://github.com/facebook/react-native/issues/27504):
-        // When the FlatList is scrolled to the end and the last item is deleted, a blank space is left behind.
-        // To fix this, we detect when onEndReached is triggered due to an item deletion,
-        // and programmatically scroll to the end to fill the space.
-        if (carouselTransactions.length >= prevCarouselTransactionLength.current || !shouldAdjustScroll) {
-            return;
-        }
-        prevCarouselTransactionLength.current = carouselTransactions.length;
-        carouselRef.current?.scrollToEnd();
-    }, [carouselTransactions.length]);
-
-    const renderSeparator = () => <View style={styles.transactionsCarouselGap} />;
-
-    const getItemType = (_item: Transaction, index: number) => {
-        return index === MAX_PREVIEWS_NUMBER ? ITEM_LAYOUT_TYPE.SHOW_MORE : ITEM_LAYOUT_TYPE.PREVIEW;
-    };
-
-    let previewContent: React.ReactNode;
-
-    if (shouldShowPreviewLoading) {
-        previewContent = (
-            <View
-                style={[
-                    {
-                        height: CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.WIDE_HEIGHT,
-                        minWidth: previewCarouselMinWidth,
-                    },
-                    styles.justifyContentCenter,
-                    styles.mtn1,
-                ]}
-            >
-                <ActivityIndicator
-                    size={40}
-                    reasonAttributes={carouselReasonAttributes}
-                />
-            </View>
-        );
-    } else if (shouldShowAccessPlaceHolder) {
-        previewContent = <AccessMoneyRequestReportPreviewPlaceHolder />;
-    } else if (shouldShowEmptyPlaceholder) {
-        previewContent = <EmptyMoneyRequestReportPreview />;
-    } else {
-        previewContent = (
-            <View style={[styles.flex1, styles.flexColumn, styles.overflowVisible, styles.minHeight42]}>
-                <FlashList
-                    key={carouselKey}
-                    snapToAlignment="start"
-                    decelerationRate="fast"
-                    snapToOffsets={snapOffsets}
-                    horizontal
-                    ItemSeparatorComponent={renderSeparator}
-                    data={carouselTransactions}
-                    ref={carouselRef}
-                    nestedScrollEnabled
-                    bounces={false}
-                    keyExtractor={(item) => `${item.transactionID}_${reportPreviewStyles.transactionPreviewCarouselStyle.width}`}
-                    contentContainerStyle={styles.ph2}
-                    style={reportPreviewStyles.flatListStyle}
-                    showsHorizontalScrollIndicator={false}
-                    renderItem={renderItem}
-                    getItemType={getItemType}
-                    onViewableItemsChanged={onViewableItemsChanged}
-                    onEndReached={adjustScroll}
-                    viewabilityConfig={viewabilityConfig}
-                    ListFooterComponent={<View style={styles.pl2} />}
-                    ListHeaderComponent={<View style={styles.pr2} />}
-                    drawDistance={1000}
-                />
-            </View>
-        );
-    }
-
     return (
-        <View
-            onLayout={onWrapperLayout}
-            testID="MoneyRequestReportPreviewContent-wrapper"
-            fsClass={forwardedFSClass}
+        <MoneyRequestReportPreviewProvider
+            iouReportID={iouReportID}
+            chatReportID={chatReportID}
+            action={action}
+            iouReport={iouReport}
+            chatReport={chatReport}
+            transactions={transactions}
+            policy={policy}
+            invoiceReceiverPolicy={invoiceReceiverPolicy}
+            invoiceReceiverPersonalDetail={invoiceReceiverPersonalDetail}
+            lastTransactionViolations={lastTransactionViolations}
+            onPaymentOptionsShow={onPaymentOptionsShow}
+            onPaymentOptionsHide={onPaymentOptionsHide}
+            renderTransactionItem={renderTransactionItem}
+            currentWidth={currentWidth}
+            reportPreviewStyles={reportPreviewStyles}
+            newTransactionIDs={newTransactionIDs}
         >
-            <OfflineWithFeedback
-                pendingAction={iouReport?.pendingFields?.preview}
-                shouldDisableOpacity={!!(action.pendingAction ?? action.isOptimisticAction)}
-                needsOffscreenAlphaCompositing
-                style={styles.mt1}
-            >
-                <View
-                    style={[styles.chatItemMessage, isReportDeleted && [styles.cursorDisabled, styles.pointerEventsAuto], containerStyles, isTransitionPending && styles.w100]}
-                    onLayout={onCarouselLayout}
-                    testID="carouselWidthSetter"
-                >
-                    <PressableWithoutFeedback
-                        onPress={onPress}
-                        onPressIn={() => canUseTouchScreen() && ControlSelection.block()}
-                        onPressOut={() => ControlSelection.unblock()}
-                        onLongPress={(event) => {
-                            if (!shouldDisplayContextMenu) {
-                                return;
-                            }
-                            showContextMenuForReport(event, contextMenuAnchorRef, chatReportID, action, checkIfContextMenuActive, originalReportID);
-                        }}
-                        shouldUseHapticsOnLongPress
-                        style={[
-                            styles.flexRow,
-                            styles.justifyContentBetween,
-                            StyleUtils.getBackgroundColorStyle(theme.cardBG),
-                            shouldShowBorder ? styles.borderedContentCardLarge : styles.reportContainerBorderRadius,
-                            isReportDeleted && styles.pointerEventsNone,
-                        ]}
-                        role={getButtonRole(true)}
-                        isNested
-                        accessibilityLabel={translate('iou.viewDetails')}
-                        sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CARD}
-                    >
-                        <View
-                            style={[
-                                StyleUtils.getBackgroundColorStyle(theme.cardBG),
-                                styles.reportContainerBorderRadius,
-                                styles.w100,
-                                (isHovered || isScanning || isWhisper) && styles.reportPreviewBoxHoverBorder,
-                            ]}
-                        >
-                            <View style={[reportPreviewStyles.wrapperStyle]}>
-                                <View style={[reportPreviewStyles.contentContainerStyle, styles.gap4]}>
-                                    <View style={[styles.expenseAndReportPreviewTextContainer, styles.overflowHidden]}>
-                                        <View style={[styles.flexRow, styles.justifyContentBetween, styles.flexShrink1, styles.gap1]}>
-                                            <View style={[styles.flexColumn, styles.gap1, styles.flexShrink1]}>
-                                                <View style={[styles.flexRow, styles.mw100, styles.flexShrink1]}>
-                                                    <Animated.View style={[styles.flexRow, styles.alignItemsCenter, previewMessageStyle, styles.flexShrink1]}>
-                                                        <Text
-                                                            style={[styles.headerText]}
-                                                            testID="MoneyRequestReportPreview-reportName"
-                                                        >
-                                                            {getReportName(iouReport, reportAttributes) || action.childReportName}
-                                                        </Text>
-                                                    </Animated.View>
-                                                </View>
-                                                {showStatusAndSkeleton && shouldShowSkeleton ? (
-                                                    <MoneyReportHeaderStatusBarSkeleton reasonAttributes={skeletonReasonAttributes} />
-                                                ) : (
-                                                    (!shouldShowEmptyPlaceholder || shouldShowAccessPlaceHolder) &&
-                                                    (shouldShowReportStatus || !shouldShowAccessPlaceHolder) && (
-                                                        <View style={[styles.flexRow, styles.justifyContentStart, styles.alignItemsCenter]}>
-                                                            {shouldShowReportStatus && (
-                                                                <StatusBadge
-                                                                    text={reportStatus}
-                                                                    backgroundColor={reportStatusColorStyle?.backgroundColor}
-                                                                    textColor={reportStatusColorStyle?.textColor}
-                                                                    badgeStyles={styles.mr1}
-                                                                />
-                                                            )}
-                                                            {!shouldShowAccessPlaceHolder && <Text style={[styles.textLabelSupporting, styles.lh16]}>{expenseCount}</Text>}
-                                                        </View>
-                                                    )
-                                                )}
-                                            </View>
-                                            {!shouldUseNarrowLayout && !shouldShowAccessPlaceHolder && transactions.length > 2 && reportPreviewStyles.expenseCountVisible && (
-                                                <View style={[styles.flexRow, styles.alignItemsCenter]}>
-                                                    <PressableWithFeedback
-                                                        accessibilityRole="button"
-                                                        accessible
-                                                        accessibilityLabel={translate('common.previous')}
-                                                        style={[styles.reportPreviewArrowButton, {backgroundColor: theme.buttonDefaultBG}]}
-                                                        onPress={() => handleChange(currentIndex - 1)}
-                                                        disabled={optimisticIndex !== undefined ? optimisticIndex === 0 : currentIndex === 0 && currentVisibleItems.at(0) === 0}
-                                                        disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]}
-                                                        sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CAROUSEL_PREVIOUS}
-                                                    >
-                                                        <Icon
-                                                            src={expensifyIcons.BackArrow}
-                                                            fill={theme.icon}
-                                                            width={variables.iconSizeExtraSmall}
-                                                            height={variables.iconSizeExtraSmall}
-                                                        />
-                                                    </PressableWithFeedback>
-                                                    <PressableWithFeedback
-                                                        accessibilityRole="button"
-                                                        accessible
-                                                        accessibilityLabel={translate('common.next')}
-                                                        style={[styles.reportPreviewArrowButton, {backgroundColor: theme.buttonDefaultBG}]}
-                                                        onPress={() => handleChange(currentIndex + 1)}
-                                                        disabled={
-                                                            optimisticIndex
-                                                                ? optimisticIndex + visibleItemsOnEndCount >= carouselTransactions.length
-                                                                : currentVisibleItems.at(-1) === carouselTransactions.length - 1
-                                                        }
-                                                        disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]}
-                                                        sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CAROUSEL_NEXT}
-                                                    >
-                                                        <Icon
-                                                            src={expensifyIcons.ArrowRight}
-                                                            fill={theme.icon}
-                                                            width={variables.iconSizeExtraSmall}
-                                                            height={variables.iconSizeExtraSmall}
-                                                        />
-                                                    </PressableWithFeedback>
-                                                </View>
-                                            )}
-                                        </View>
-                                    </View>
-                                    {previewContent}
-                                    <View style={[styles.expenseAndReportPreviewTextContainer]}>
-                                        <View style={[totalAmountStyle, styles.justifyContentBetween, styles.gap4, StyleUtils.getMinimumHeight(variables.h28)]}>
-                                            <ReportPreviewActionButton
-                                                iouReportID={iouReportID}
-                                                chatReportID={chatReportID}
-                                                chatReport={chatReport}
-                                                isPaidAnimationRunning={isPaidAnimationRunning}
-                                                isApprovedAnimationRunning={isApprovedAnimationRunning}
-                                                isSubmittingAnimationRunning={isSubmittingAnimationRunning}
-                                                stopAnimation={stopAnimation}
-                                                startAnimation={startAnimation}
-                                                startApprovedAnimation={startApprovedAnimation}
-                                                startSubmittingAnimation={startSubmittingAnimation}
-                                                onPaymentOptionsShow={onPaymentOptionsShow}
-                                                onPaymentOptionsHide={onPaymentOptionsHide}
-                                                openReportFromPreview={openReportFromPreview}
-                                                onHoldMenuOpen={handleHoldMenuOpen}
-                                                transactionPreviewCarouselWidth={reportPreviewStyles.transactionPreviewCarouselStyle.width}
-                                            />
-                                            {transactions.length > 1 && !shouldShowAccessPlaceHolder && (
-                                                <View style={[styles.flexRow, shouldUseNarrowLayout ? styles.justifyContentBetween : styles.gap2, styles.alignItemsCenter]}>
-                                                    <Text
-                                                        style={[styles.textLabelSupporting]}
-                                                        numberOfLines={1}
-                                                    >
-                                                        {translate('common.total')}
-                                                    </Text>
-                                                    <Text style={[styles.headerText]}>{convertToDisplayString(totalDisplaySpend, iouReport?.currency)}</Text>
-                                                </View>
-                                            )}
-                                        </View>
-                                    </View>
-                                </View>
-                            </View>
-                        </View>
-                    </PressableWithoutFeedback>
-                </View>
-                {isHoldMenuVisible &&
-                    !!iouReport &&
-                    !!requestType &&
-                    (() => {
-                        const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton, transactions);
-                        return (
-                            <ProcessMoneyReportHoldMenu
-                                nonHeldAmount={!hasOnlyHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined}
-                                requestType={requestType}
-                                fullAmount={fullAmount}
-                                onClose={() => setIsHoldMenuVisible(false)}
-                                isVisible={isHoldMenuVisible}
-                                paymentType={paymentType}
-                                methodID={methodID}
-                                chatReport={chatReport}
-                                moneyRequestReport={iouReport}
-                                transactionCount={numberOfRequests}
-                                hasNonHeldExpenses={!hasOnlyHeldExpenses}
-                                onConfirm={() => {
-                                    if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
-                                        startApprovedAnimation();
-                                    } else {
-                                        startAnimation();
-                                    }
-                                }}
-                            />
-                        );
-                    })()}
-            </OfflineWithFeedback>
-        </View>
+            <MoneyRequestReportPreviewBody
+                onWrapperLayout={onWrapperLayout}
+                onCarouselLayout={onCarouselLayout}
+                containerStyles={containerStyles}
+                onPress={onPress}
+                isHovered={isHovered}
+                isWhisper={isWhisper}
+                shouldShowBorder={shouldShowBorder}
+                forwardedFSClass={forwardedFSClass}
+            />
+        </MoneyRequestReportPreviewProvider>
     );
 }
 
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext.ts
new file mode 100644
index 000000000000..67836b4aeff0
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContext.ts
@@ -0,0 +1,113 @@
+import type {Context, RefObject} from 'react';
+import {createContext, useContext} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
+import type {PersonalDetails, Policy, Report, ReportAction, Transaction} from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import type {ReportPreviewHoldMenuHandle} from './ReportPreviewHoldMenu';
+import type {MoneyRequestReportPreviewStyleType} from './types';
+import type usePreviewMessageAnimation from './usePreviewMessageAnimation';
+import type useReportPreviewCarousel from './useReportPreviewCarousel';
+
+type ReportPreviewCarouselState = {
+    isPreviousDisabled: boolean;
+    isNextDisabled: boolean;
+};
+
+type ReportPreviewAnimationState = {
+    isPaidAnimationRunning: boolean;
+    isApprovedAnimationRunning: boolean;
+    isSubmittingAnimationRunning: boolean;
+};
+
+type ReportPreviewData = {
+    iouReportID: string | undefined;
+    chatReportID: string | undefined;
+    action: ReportAction;
+    iouReport: OnyxEntry<Report>;
+    chatReport: OnyxEntry<Report>;
+    transactions: Transaction[];
+    policy: OnyxEntry<Policy>;
+    invoiceReceiverPolicy: OnyxEntry<Policy>;
+    invoiceReceiverPersonalDetail: OnyxEntry<PersonalDetails> | null;
+};
+
+type ReportPreviewUIState = {
+    isTransitionPending: boolean;
+    shouldShowPreviewLoading: boolean;
+    shouldShowAccessPlaceHolder: boolean;
+    shouldShowEmptyPlaceholder: boolean;
+    shouldShowSkeleton: boolean;
+    showStatusAndSkeleton: boolean;
+    shouldShowCarouselArrows: boolean;
+    isScanning: boolean;
+    previewCarouselMinWidth: number;
+    skeletonReasonAttributes: SkeletonSpanReasonAttributes;
+    carouselReasonAttributes: SkeletonSpanReasonAttributes;
+    previewMessageStyle: ReturnType<typeof usePreviewMessageAnimation>['previewMessageStyle'];
+    reportPreviewStyles: MoneyRequestReportPreviewStyleType;
+};
+
+type ReportPreviewCarouselList = Pick<
+    ReturnType<typeof useReportPreviewCarousel>,
+    'carouselTransactions' | 'carouselKey' | 'snapOffsets' | 'renderItem' | 'getItemType' | 'renderSeparator' | 'viewabilityConfig' | 'onViewableItemsChanged' | 'adjustScroll'
+>;
+
+type ReportPreviewActions = {
+    openReportFromPreview: () => void;
+    onHoldMenuOpen: (requestType: string, paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => void;
+    onPaymentOptionsShow?: () => void;
+    onPaymentOptionsHide?: () => void;
+    stopAnimation: () => void;
+    startAnimation: () => void;
+    startApprovedAnimation: () => void;
+    startSubmittingAnimation: () => void;
+    goToPrevious: () => void;
+    goToNext: () => void;
+};
+
+type ReportPreviewMeta = {
+    setCarouselRef: ReturnType<typeof useReportPreviewCarousel>['setCarouselRef'];
+    holdMenuRef: RefObject<ReportPreviewHoldMenuHandle | null>;
+};
+
+const ReportPreviewCarouselStateContext = createContext<ReportPreviewCarouselState | undefined>(undefined);
+const ReportPreviewAnimationStateContext = createContext<ReportPreviewAnimationState | undefined>(undefined);
+const ReportPreviewDataContext = createContext<ReportPreviewData | undefined>(undefined);
+const ReportPreviewUIStateContext = createContext<ReportPreviewUIState | undefined>(undefined);
+const ReportPreviewCarouselListContext = createContext<ReportPreviewCarouselList | undefined>(undefined);
+const ReportPreviewActionsContext = createContext<ReportPreviewActions | undefined>(undefined);
+const ReportPreviewMetaContext = createContext<ReportPreviewMeta | undefined>(undefined);
+
+function useSliceContext<T>(context: Context<T | undefined>, name: string): T {
+    const value = useContext(context);
+    if (!value) {
+        throw new Error(`${name} must be used within a MoneyRequestReportPreviewProvider`);
+    }
+    return value;
+}
+
+const useReportPreviewCarouselState = () => useSliceContext(ReportPreviewCarouselStateContext, 'useReportPreviewCarouselState');
+const useReportPreviewAnimationState = () => useSliceContext(ReportPreviewAnimationStateContext, 'useReportPreviewAnimationState');
+const useReportPreviewData = () => useSliceContext(ReportPreviewDataContext, 'useReportPreviewData');
+const useReportPreviewUIState = () => useSliceContext(ReportPreviewUIStateContext, 'useReportPreviewUIState');
+const useReportPreviewCarouselList = () => useSliceContext(ReportPreviewCarouselListContext, 'useReportPreviewCarouselList');
+const useReportPreviewActions = () => useSliceContext(ReportPreviewActionsContext, 'useReportPreviewActions');
+const useReportPreviewMeta = () => useSliceContext(ReportPreviewMetaContext, 'useReportPreviewMeta');
+
+export {
+    ReportPreviewCarouselStateContext,
+    ReportPreviewAnimationStateContext,
+    ReportPreviewDataContext,
+    ReportPreviewUIStateContext,
+    ReportPreviewCarouselListContext,
+    ReportPreviewActionsContext,
+    ReportPreviewMetaContext,
+    useReportPreviewCarouselState,
+    useReportPreviewAnimationState,
+    useReportPreviewData,
+    useReportPreviewUIState,
+    useReportPreviewCarouselList,
+    useReportPreviewActions,
+    useReportPreviewMeta,
+};
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx
new file mode 100644
index 000000000000..941d7098c59e
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx
@@ -0,0 +1,289 @@
+import {useFocusEffect} from '@react-navigation/native';
+import type {ListRenderItem} from '@shopify/flash-list';
+import React, {useCallback, useDeferredValue, useMemo, useRef, useState} from 'react';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import useOnyx from '@hooks/useOnyx';
+import usePaymentAnimations from '@hooks/usePaymentAnimations';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import Navigation from '@libs/Navigation/Navigation';
+import TransitionTracker from '@libs/Navigation/TransitionTracker';
+import {
+    areAllRequestsBeingSmartScanned as areAllRequestsBeingSmartScannedReportUtils,
+    getMoneyRequestSpendBreakdown,
+    getTransactionsWithReceipts,
+    hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils,
+    isInvoiceRoom as isInvoiceRoomReportUtils,
+    isPolicyExpenseChat as isPolicyExpenseChatReportUtils,
+    isReportApproved,
+    isSettled,
+    isTripRoom as isTripRoomReportUtils,
+} from '@libs/ReportUtils';
+import {startSpan} from '@libs/telemetry/activeSpans';
+import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
+import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
+import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import {transactionViolationsByIDsSelector} from '@src/selectors/TransactionViolations';
+import type {PersonalDetails, Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import {
+    ReportPreviewActionsContext,
+    ReportPreviewAnimationStateContext,
+    ReportPreviewCarouselListContext,
+    ReportPreviewCarouselStateContext,
+    ReportPreviewDataContext,
+    ReportPreviewMetaContext,
+    ReportPreviewUIStateContext,
+} from './MoneyRequestReportPreviewContext';
+import type {ReportPreviewHoldMenuHandle} from './ReportPreviewHoldMenu';
+import type {MoneyRequestReportPreviewStyleType} from './types';
+import usePreviewMessageAnimation from './usePreviewMessageAnimation';
+import useReportPreviewCarousel from './useReportPreviewCarousel';
+
+type MoneyRequestReportPreviewProviderProps = ChildrenProps & {
+    iouReportID: string | undefined;
+    chatReportID: string | undefined;
+    action: ReportAction;
+    iouReport: OnyxEntry<Report>;
+    chatReport: OnyxEntry<Report>;
+    transactions: Transaction[];
+    policy: OnyxEntry<Policy>;
+    invoiceReceiverPolicy: OnyxEntry<Policy>;
+    invoiceReceiverPersonalDetail: OnyxEntry<PersonalDetails> | null;
+    lastTransactionViolations: TransactionViolations;
+    onPaymentOptionsShow?: () => void;
+    onPaymentOptionsHide?: () => void;
+    renderTransactionItem: ListRenderItem<Transaction>;
+    currentWidth: number;
+    reportPreviewStyles: MoneyRequestReportPreviewStyleType;
+    newTransactionIDs?: Set<string>;
+};
+
+/**
+ * Owns all state/derivations for a money request report preview once and exposes them through the context slices.
+ * Mirrors `ReportActionCompose`'s `ComposerProvider`. Report entities arrive as (stabilized) props from `index.tsx`;
+ * this provider only adds derivations + local UI state (loading flags, animations, carousel, message animation, refs).
+ */
+function MoneyRequestReportPreviewProvider({
+    children,
+    iouReportID,
+    chatReportID,
+    action,
+    iouReport,
+    chatReport,
+    transactions,
+    policy,
+    invoiceReceiverPolicy,
+    invoiceReceiverPersonalDetail,
+    lastTransactionViolations,
+    onPaymentOptionsShow,
+    onPaymentOptionsHide,
+    renderTransactionItem,
+    currentWidth,
+    reportPreviewStyles,
+    newTransactionIDs,
+}: MoneyRequestReportPreviewProviderProps) {
+    const [chatReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`);
+    const [chatReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`);
+    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+    const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
+
+    const [isTransitionPending, setIsTransitionPending] = useState(() => {
+        const pending = getPendingSubmitFollowUpAction();
+        return pending?.followUpAction === CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT && (pending?.reportID === chatReportID || pending?.reportID === iouReportID);
+    });
+
+    useFocusEffect(
+        useCallback(() => {
+            if (!isTransitionPending) {
+                return;
+            }
+            const handle = TransitionTracker.runAfterTransitions({
+                callback: () => setIsTransitionPending(false),
+                waitForUpcomingTransition: true,
+            });
+            return () => handle.cancel();
+        }, [isTransitionPending]),
+    );
+
+    const shouldShowLoading =
+        chatReportLoadingState != null && chatReportLoadingState.hasOnceLoadedReportActions !== true && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
+    const transactionIDs = useMemo(() => transactions.map((transaction) => transaction.transactionID), [transactions]);
+    const selectTransactionViolations = useCallback(
+        (allViolations: OnyxCollection<TransactionViolations>) => transactionViolationsByIDsSelector(transactionIDs)(allViolations),
+        [transactionIDs],
+    );
+    const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {selector: selectTransactionViolations});
+    // `hasOnceLoadedReportActions` becomes true before transactions populate fully,
+    // so we defer the loading state update to ensure transactions are loaded
+    const shouldShowLoadingDeferred = useDeferredValue(shouldShowLoading);
+    const lastTransaction = transactions?.at(0);
+    const shouldShowSkeleton = shouldShowLoading && transactions.length === 0;
+    const shouldShowAccessPlaceHolder = !iouReport && !shouldShowLoading;
+    const shouldShowEmptyPlaceholder = transactions.length === 0 && !shouldShowLoading;
+    const shouldShowPreviewPlaceholder = shouldShowAccessPlaceHolder || shouldShowEmptyPlaceholder;
+    const showStatusAndSkeleton = !shouldShowEmptyPlaceholder;
+    // Empty/access placeholders do not depend on measured carousel width, so we can show them immediately
+    // once the report data is ready instead of keeping the taller loading state around and causing the preview to reflow.
+    const shouldShowPreviewLoading = isTransitionPending || shouldShowLoading || shouldShowLoadingDeferred || (!currentWidth && !shouldShowPreviewPlaceholder);
+    const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
+        context: 'MoneyRequestReportPreviewContent',
+        hasOnceLoadedReportActions: chatReportLoadingState?.hasOnceLoadedReportActions,
+        isTransactionsEmpty: transactions.length === 0,
+        isOptimisticReport: chatReportMetadata?.isOptimisticReport,
+    };
+    const carouselReasonAttributes: SkeletonSpanReasonAttributes = {
+        context: 'MoneyRequestReportPreviewContent.Carousel',
+        hasCurrentWidth: !!currentWidth,
+        shouldShowLoading,
+        shouldShowLoadingDeferred,
+    };
+    const previewCarouselMinWidth = shouldUseNarrowLayout ? CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.MIN_NARROW_WIDTH : CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.MIN_WIDE_WIDTH;
+
+    // These read the report's transactions from Onyx by ID and are recomputed each render (the provider re-renders
+    // when `transactions` change), matching the inline `transactionsWithReceipts`/`numberOfPendingRequests` below.
+    const areAllRequestsBeingSmartScanned = areAllRequestsBeingSmartScannedReportUtils(iouReportID, action);
+    const hasNonReimbursableTransactions = hasNonReimbursableTransactionsReportUtils(iouReportID);
+
+    const {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation, startSubmittingAnimation} =
+        usePaymentAnimations();
+
+    const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
+    const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(iouReport);
+
+    const iouSettled = isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
+    const isApproved = isReportApproved({
+        report: iouReport,
+        parentReportAction: action,
+    });
+
+    const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport);
+    const isInvoiceRoom = isInvoiceRoomReportUtils(chatReport);
+    const isTripRoom = isTripRoomReportUtils(chatReport);
+
+    const numberOfRequests = transactions?.length ?? 0;
+    const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
+    const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => isPending(transaction) && isManagedCardTransaction(transaction)).length;
+
+    const shouldShowRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, lastTransactionViolations);
+
+    const hasReceipts = transactionsWithReceipts.length > 0;
+    const isScanning = hasReceipts && areAllRequestsBeingSmartScanned;
+
+    const {previewMessageStyle} = usePreviewMessageAnimation({
+        isScanning,
+        numberOfPendingRequests,
+        numberOfRequests,
+        shouldShowRTERViolationMessage,
+        isPolicyExpenseChat,
+        isTripRoom,
+        isInvoiceRoom,
+        isApproved,
+        iouSettled,
+        iouReport,
+        hasNonReimbursableTransactions,
+        totalDisplaySpend,
+        chatReport,
+        policy,
+        invoiceReceiverPolicy,
+        invoiceReceiverPersonalDetail,
+        managerID,
+        isPaidAnimationRunning,
+        isApprovedAnimationRunning,
+        isSubmittingAnimationRunning,
+    });
+
+    const {setCarouselRef, goToPrevious, goToNext, isPreviousDisabled, isNextDisabled, ...carouselList} = useReportPreviewCarousel({
+        transactions,
+        transactionViolations,
+        iouReport,
+        policy,
+        shouldShowAccessPlaceHolder,
+        reportPreviewStyles,
+        currentWidth,
+        newTransactionIDs,
+        renderTransactionItem,
+    });
+
+    const openReportFromPreview = useCallback(() => {
+        if (!iouReportID) {
+            return;
+        }
+        startSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${iouReportID}`, {
+            name: 'MoneyRequestReportPreviewContent',
+            op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
+        });
+        // Small screens navigate to full report view since super wide RHP
+        // is not available on narrow layouts and would break the navigation logic.
+        if (isSmallScreenWidth) {
+            Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
+        } else {
+            Navigation.navigate(
+                ROUTES.EXPENSE_REPORT_RHP.getRoute({
+                    reportID: iouReportID,
+                    backTo: Navigation.getActiveRoute(),
+                }),
+            );
+        }
+    }, [iouReportID, isSmallScreenWidth]);
+
+    const holdMenuRef = useRef<ReportPreviewHoldMenuHandle>(null);
+    const onHoldMenuOpen = useCallback((requestType: string, paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => {
+        holdMenuRef.current?.open(requestType, paymentType, canPay, methodID);
+    }, []);
+
+    const shouldShowCarouselArrows = !shouldUseNarrowLayout && !shouldShowAccessPlaceHolder && transactions.length > 2 && reportPreviewStyles.expenseCountVisible;
+
+    const dataValue = {iouReportID, chatReportID, action, iouReport, chatReport, transactions, policy, invoiceReceiverPolicy, invoiceReceiverPersonalDetail};
+    const uiStateValue = {
+        isTransitionPending,
+        shouldShowPreviewLoading,
+        shouldShowAccessPlaceHolder,
+        shouldShowEmptyPlaceholder,
+        shouldShowSkeleton,
+        showStatusAndSkeleton,
+        shouldShowCarouselArrows,
+        isScanning,
+        previewCarouselMinWidth,
+        skeletonReasonAttributes,
+        carouselReasonAttributes,
+        previewMessageStyle,
+        reportPreviewStyles,
+    };
+    const animationStateValue = {isPaidAnimationRunning, isApprovedAnimationRunning, isSubmittingAnimationRunning};
+    const carouselStateValue = {isPreviousDisabled, isNextDisabled};
+    const actionsValue = {
+        openReportFromPreview,
+        onHoldMenuOpen,
+        onPaymentOptionsShow,
+        onPaymentOptionsHide,
+        stopAnimation,
+        startAnimation,
+        startApprovedAnimation,
+        startSubmittingAnimation,
+        goToPrevious,
+        goToNext,
+    };
+    const metaValue = {setCarouselRef, holdMenuRef};
+
+    return (
+        <ReportPreviewDataContext.Provider value={dataValue}>
+            <ReportPreviewUIStateContext.Provider value={uiStateValue}>
+                <ReportPreviewCarouselStateContext.Provider value={carouselStateValue}>
+                    <ReportPreviewAnimationStateContext.Provider value={animationStateValue}>
+                        <ReportPreviewCarouselListContext.Provider value={carouselList}>
+                            <ReportPreviewActionsContext.Provider value={actionsValue}>
+                                <ReportPreviewMetaContext.Provider value={metaValue}>{children}</ReportPreviewMetaContext.Provider>
+                            </ReportPreviewActionsContext.Provider>
+                        </ReportPreviewCarouselListContext.Provider>
+                    </ReportPreviewAnimationStateContext.Provider>
+                </ReportPreviewCarouselStateContext.Provider>
+            </ReportPreviewUIStateContext.Provider>
+        </ReportPreviewDataContext.Provider>
+    );
+}
+
+export default MoneyRequestReportPreviewProvider;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx
index 2b288702d3c7..ff6ec4c3c533 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewActionButton.tsx
@@ -32,6 +32,9 @@ type ReportPreviewActionButtonProps = {
     iouReportID: string | undefined;
     chatReportID: string | undefined;
     chatReport: OnyxEntry<Report>;
+
+    /** The stabilized IOU report, provided by the parent so the dispatcher does not re-subscribe to the churning report */
+    iouReport: OnyxEntry<Report>;
     isPaidAnimationRunning: boolean;
     isApprovedAnimationRunning: boolean;
     isSubmittingAnimationRunning: boolean;
@@ -50,6 +53,7 @@ function ReportPreviewActionButton({
     iouReportID,
     chatReportID,
     chatReport,
+    iouReport,
     isPaidAnimationRunning,
     isApprovedAnimationRunning,
     isSubmittingAnimationRunning,
@@ -69,7 +73,6 @@ function ReportPreviewActionButton({
     const currentUserDetails = useCurrentUserPersonalDetails();
     const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'ReceiptPlus', 'Plus']);
 
-    const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
     const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`);
     const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined;
     const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`);
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx
new file mode 100644
index 000000000000..a926a859323f
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx
@@ -0,0 +1,154 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import Animated from 'react-native-reanimated';
+import Icon from '@components/Icon';
+import MoneyReportHeaderStatusBarSkeleton from '@components/MoneyReportHeaderStatusBarSkeleton';
+import {PressableWithFeedback} from '@components/Pressable';
+import StatusBadge from '@components/StatusBadge';
+import Text from '@components/Text';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getReportStatusColorStyle, getReportStatusTranslation} from '@libs/ReportUtils';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ReportAttributesDerivedValue} from '@src/types/onyx';
+import {useReportPreviewActions, useReportPreviewCarouselState, useReportPreviewData, useReportPreviewUIState} from './MoneyRequestReportPreviewContext';
+
+/**
+ * Renders the top section of the money request report preview: the (animated) report name, the status badge with the
+ * expense count, and the carousel navigation arrows. Reads everything from the preview context slices.
+ */
+function ReportPreviewHeader() {
+    const theme = useTheme();
+    const styles = useThemeStyles();
+    const StyleUtils = useStyleUtils();
+    const {translate} = useLocalize();
+    const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'BackArrow']);
+    const {iouReport, action, transactions} = useReportPreviewData();
+    const {previewMessageStyle, shouldShowSkeleton, showStatusAndSkeleton, skeletonReasonAttributes, shouldShowEmptyPlaceholder, shouldShowAccessPlaceHolder, shouldShowCarouselArrows} =
+        useReportPreviewUIState();
+    const {isPreviousDisabled, isNextDisabled} = useReportPreviewCarouselState();
+    const {goToPrevious, goToNext} = useReportPreviewActions();
+    const numberOfRequests = transactions.length;
+
+    const reportID = iouReport?.reportID;
+    const selectReportName = useCallback((c: OnyxEntry<ReportAttributesDerivedValue>) => (reportID ? c?.reports?.[reportID]?.reportName : undefined), [reportID]);
+    const [derivedReportName] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: selectReportName});
+    const reportName = reportID ? (derivedReportName ?? iouReport?.reportName ?? '') : '';
+
+    /*
+     Show subtitle if at least one of the expenses is not being smart scanned, and either:
+     - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown;
+     - There is only one expense, it has a receipt and is not being smart scanned – in this case, the expense merchant or description is shown;
+
+     * There is an edge case when there is only one distance expense with a pending route and amount = 0.
+       In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field.
+     */
+    const expenseCount = useMemo(
+        () =>
+            translate('iou.expenseCount', {
+                count: numberOfRequests,
+            }),
+        [translate, numberOfRequests],
+    );
+
+    const reportStatus = useMemo(
+        () =>
+            getReportStatusTranslation({
+                stateNum: iouReport?.stateNum ?? action?.childStateNum,
+                statusNum: iouReport?.statusNum ?? action?.childStatusNum,
+                translate,
+            }),
+        [action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, translate],
+    );
+
+    const shouldShowReportStatus = !!reportStatus && !!expenseCount;
+
+    const reportStatusColorStyle = useMemo(
+        () => getReportStatusColorStyle(theme, iouReport?.stateNum ?? action?.childStateNum, iouReport?.statusNum ?? action?.childStatusNum),
+        [action?.childStateNum, action?.childStatusNum, iouReport?.stateNum, iouReport?.statusNum, theme],
+    );
+
+    return (
+        <View style={[styles.expenseAndReportPreviewTextContainer, styles.overflowHidden]}>
+            <View style={[styles.flexRow, styles.justifyContentBetween, styles.flexShrink1, styles.gap1]}>
+                <View style={[styles.flexColumn, styles.gap1, styles.flexShrink1]}>
+                    <View style={[styles.flexRow, styles.mw100, styles.flexShrink1]}>
+                        <Animated.View style={[styles.flexRow, styles.alignItemsCenter, previewMessageStyle, styles.flexShrink1]}>
+                            <Text
+                                style={[styles.headerText]}
+                                testID="MoneyRequestReportPreview-reportName"
+                            >
+                                {reportName || action.childReportName}
+                            </Text>
+                        </Animated.View>
+                    </View>
+                    {showStatusAndSkeleton && shouldShowSkeleton ? (
+                        <MoneyReportHeaderStatusBarSkeleton reasonAttributes={skeletonReasonAttributes} />
+                    ) : (
+                        (!shouldShowEmptyPlaceholder || shouldShowAccessPlaceHolder) &&
+                        (shouldShowReportStatus || !shouldShowAccessPlaceHolder) && (
+                            <View style={[styles.flexRow, styles.justifyContentStart, styles.alignItemsCenter]}>
+                                {shouldShowReportStatus && (
+                                    <StatusBadge
+                                        text={reportStatus}
+                                        backgroundColor={reportStatusColorStyle?.backgroundColor}
+                                        textColor={reportStatusColorStyle?.textColor}
+                                        badgeStyles={styles.mr1}
+                                    />
+                                )}
+                                {!shouldShowAccessPlaceHolder && <Text style={[styles.textLabelSupporting, styles.lh16]}>{expenseCount}</Text>}
+                            </View>
+                        )
+                    )}
+                </View>
+                {shouldShowCarouselArrows && (
+                    <View style={[styles.flexRow, styles.alignItemsCenter]}>
+                        <PressableWithFeedback
+                            accessibilityRole="button"
+                            accessible
+                            accessibilityLabel={translate('common.previous')}
+                            style={[styles.reportPreviewArrowButton, StyleUtils.getBackgroundColorStyle(theme.buttonDefaultBG)]}
+                            onPress={goToPrevious}
+                            disabled={isPreviousDisabled}
+                            disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]}
+                            sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CAROUSEL_PREVIOUS}
+                        >
+                            <Icon
+                                src={expensifyIcons.BackArrow}
+                                fill={theme.icon}
+                                width={variables.iconSizeExtraSmall}
+                                height={variables.iconSizeExtraSmall}
+                            />
+                        </PressableWithFeedback>
+                        <PressableWithFeedback
+                            accessibilityRole="button"
+                            accessible
+                            accessibilityLabel={translate('common.next')}
+                            style={[styles.reportPreviewArrowButton, StyleUtils.getBackgroundColorStyle(theme.buttonDefaultBG)]}
+                            onPress={goToNext}
+                            disabled={isNextDisabled}
+                            disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]}
+                            sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.CAROUSEL_NEXT}
+                        >
+                            <Icon
+                                src={expensifyIcons.ArrowRight}
+                                fill={theme.icon}
+                                width={variables.iconSizeExtraSmall}
+                                height={variables.iconSizeExtraSmall}
+                            />
+                        </PressableWithFeedback>
+                    </View>
+                )}
+            </View>
+        </View>
+    );
+}
+
+export default ReportPreviewHeader;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHoldMenu.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHoldMenu.tsx
new file mode 100644
index 000000000000..0f42620dcddf
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHoldMenu.tsx
@@ -0,0 +1,85 @@
+import React, {useImperativeHandle, useState} from 'react';
+import type {Ref} from 'react';
+import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
+import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
+import {getNonHeldAndFullAmount, hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import {useReportPreviewActions, useReportPreviewData} from './MoneyRequestReportPreviewContext';
+
+type ReportPreviewHoldMenuHandle = {
+    /** Opens the hold menu for the given request */
+    open: (requestType: string, paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => void;
+};
+
+type ReportPreviewHoldMenuProps = {
+    /** Imperative handle the sibling action button uses to open the menu */
+    ref?: Ref<ReportPreviewHoldMenuHandle>;
+};
+
+/**
+ * Self-contained hold/approve confirmation menu for the money request report preview. It owns all of its own
+ * visibility state and is opened imperatively (via ref) by the sibling action button, so the preview shell does not
+ * have to carry the menu's state. Reads the report subject + animation actions from the preview context.
+ */
+function ReportPreviewHoldMenu({ref}: ReportPreviewHoldMenuProps) {
+    const {iouReport, chatReport, transactions} = useReportPreviewData();
+    const {startAnimation, startApprovedAnimation} = useReportPreviewActions();
+    const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
+    const [requestType, setRequestType] = useState<ActionHandledType>();
+    const [paymentType, setPaymentType] = useState<PaymentMethodType>();
+    const [methodID, setMethodID] = useState<number>();
+    const [shouldShowPayButton, setShouldShowPayButton] = useState(false);
+
+    useImperativeHandle(
+        ref,
+        () => ({
+            open: (holdRequestType, holdPaymentType, canPay, holdMethodID) => {
+                // The shared onHoldMenuOpen contract widens the request type to `string`; the action buttons only ever
+                // pass PAY/APPROVE, so guard before narrowing to ActionHandledType.
+                if (holdRequestType !== CONST.IOU.REPORT_ACTION_TYPE.PAY && holdRequestType !== CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
+                    return;
+                }
+                setRequestType(holdRequestType);
+                setPaymentType(holdPaymentType);
+                setMethodID(holdMethodID);
+                setShouldShowPayButton(!!canPay);
+                setIsHoldMenuVisible(true);
+            },
+        }),
+        [],
+    );
+
+    if (!isHoldMenuVisible || !iouReport || !requestType) {
+        return null;
+    }
+
+    const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(transactions);
+    const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(iouReport, shouldShowPayButton, transactions);
+
+    return (
+        <ProcessMoneyReportHoldMenu
+            nonHeldAmount={!hasOnlyHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined}
+            requestType={requestType}
+            fullAmount={fullAmount}
+            onClose={() => setIsHoldMenuVisible(false)}
+            isVisible={isHoldMenuVisible}
+            paymentType={paymentType}
+            methodID={methodID}
+            chatReport={chatReport}
+            moneyRequestReport={iouReport}
+            transactionCount={transactions.length}
+            hasNonHeldExpenses={!hasOnlyHeldExpenses}
+            onConfirm={() => {
+                if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
+                    startApprovedAnimation();
+                } else {
+                    startAnimation();
+                }
+            }}
+        />
+    );
+}
+
+export default ReportPreviewHoldMenu;
+export type {ReportPreviewHoldMenuHandle};
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewTotal.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewTotal.tsx
new file mode 100644
index 000000000000..231e8d6f4419
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewTotal.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import {View} from 'react-native';
+import Text from '@components/Text';
+import {useCurrencyListActions} from '@hooks/useCurrencyList';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getMoneyRequestSpendBreakdown} from '@libs/ReportUtils';
+import {useReportPreviewData, useReportPreviewUIState} from './MoneyRequestReportPreviewContext';
+
+/**
+ * Renders the total amount of a multi-expense report preview. Reads the report subject from context and derives the
+ * total itself, so the shell does not drill amount data down.
+ */
+function ReportPreviewTotal() {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+    const {convertToDisplayString} = useCurrencyListActions();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+    const {iouReport, transactions} = useReportPreviewData();
+    const {shouldShowAccessPlaceHolder} = useReportPreviewUIState();
+    const {totalDisplaySpend} = getMoneyRequestSpendBreakdown(iouReport);
+
+    if (transactions.length <= 1 || shouldShowAccessPlaceHolder) {
+        return null;
+    }
+
+    return (
+        <View style={[styles.flexRow, shouldUseNarrowLayout ? styles.justifyContentBetween : styles.gap2, styles.alignItemsCenter]}>
+            <Text
+                style={[styles.textLabelSupporting]}
+                numberOfLines={1}
+            >
+                {translate('common.total')}
+            </Text>
+            <Text style={[styles.headerText]}>{convertToDisplayString(totalDisplaySpend, iouReport?.currency)}</Text>
+        </View>
+    );
+}
+
+export default ReportPreviewTotal;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/TransactionReportCarousel.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/TransactionReportCarousel.tsx
new file mode 100644
index 000000000000..9b9d5871de9a
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/TransactionReportCarousel.tsx
@@ -0,0 +1,80 @@
+import {FlashList} from '@shopify/flash-list';
+import React from 'react';
+import {View} from 'react-native';
+import ActivityIndicator from '@components/ActivityIndicator';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import AccessMoneyRequestReportPreviewPlaceHolder from './AccessMoneyRequestReportPreviewPlaceHolder';
+import EmptyMoneyRequestReportPreview from './EmptyMoneyRequestReportPreview';
+import {useReportPreviewCarouselList, useReportPreviewMeta, useReportPreviewUIState} from './MoneyRequestReportPreviewContext';
+
+/**
+ * Renders the body of the money request report preview: a loading indicator, the access/empty placeholders,
+ * or the horizontal transaction carousel. Reads everything from the preview context slices.
+ */
+function TransactionReportCarousel() {
+    const styles = useThemeStyles();
+    const StyleUtils = useStyleUtils();
+    const {shouldShowPreviewLoading, shouldShowAccessPlaceHolder, shouldShowEmptyPlaceholder, previewCarouselMinWidth, carouselReasonAttributes, reportPreviewStyles} =
+        useReportPreviewUIState();
+    const carousel = useReportPreviewCarouselList();
+    const {setCarouselRef} = useReportPreviewMeta();
+
+    if (shouldShowPreviewLoading) {
+        return (
+            <View
+                style={[
+                    StyleUtils.getHeight(CONST.REPORT.TRANSACTION_PREVIEW.CAROUSEL.WIDE_HEIGHT),
+                    StyleUtils.getMinimumWidth(previewCarouselMinWidth),
+                    styles.justifyContentCenter,
+                    styles.mtn1,
+                ]}
+            >
+                <ActivityIndicator
+                    size={40}
+                    reasonAttributes={carouselReasonAttributes}
+                />
+            </View>
+        );
+    }
+
+    if (shouldShowAccessPlaceHolder) {
+        return <AccessMoneyRequestReportPreviewPlaceHolder />;
+    }
+
+    if (shouldShowEmptyPlaceholder) {
+        return <EmptyMoneyRequestReportPreview />;
+    }
+
+    return (
+        <View style={[styles.flex1, styles.flexColumn, styles.overflowVisible, styles.minHeight42]}>
+            <FlashList
+                key={carousel.carouselKey}
+                snapToAlignment="start"
+                decelerationRate="fast"
+                snapToOffsets={carousel.snapOffsets}
+                horizontal
+                ItemSeparatorComponent={carousel.renderSeparator}
+                data={carousel.carouselTransactions}
+                ref={setCarouselRef}
+                nestedScrollEnabled
+                bounces={false}
+                keyExtractor={(item) => `${item.transactionID}_${reportPreviewStyles.transactionPreviewCarouselStyle.width}`}
+                contentContainerStyle={styles.ph2}
+                style={reportPreviewStyles.flatListStyle}
+                showsHorizontalScrollIndicator={false}
+                renderItem={carousel.renderItem}
+                getItemType={carousel.getItemType}
+                onViewableItemsChanged={carousel.onViewableItemsChanged}
+                onEndReached={carousel.adjustScroll}
+                viewabilityConfig={carousel.viewabilityConfig}
+                ListFooterComponent={<View style={styles.pl2} />}
+                ListHeaderComponent={<View style={styles.pr2} />}
+                drawDistance={1000}
+            />
+        </View>
+    );
+}
+
+export default TransactionReportCarousel;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
index e79b5f41a3db..004df19c44e0 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
@@ -4,10 +4,11 @@ import React, {useCallback, useMemo, useRef, useState} from 'react';
 import type {LayoutChangeEvent} from 'react-native';
 import {usePersonalDetails} from '@components/OnyxListItemProvider';
 import TransactionPreview from '@components/ReportActionItem/TransactionPreview';
+import useNetwork from '@hooks/useNetwork';
 import useNewTransactions from '@hooks/useNewTransactions';
 import useOnyx from '@hooks/useOnyx';
 import usePolicy from '@hooks/usePolicy';
-import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations';
+import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -28,6 +29,7 @@ import type {MoneyRequestReportPreviewProps} from './types';
 
 function MoneyRequestReportPreview({
     iouReportID,
+    iouReport,
     policyID,
     chatReportID,
     chatReport,
@@ -46,7 +48,15 @@ function MoneyRequestReportPreview({
     const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined;
     const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(invoiceReceiverPolicyID)}`);
     const invoiceReceiverPersonalDetail = chatReport?.invoiceReceiver && 'accountID' in chatReport.invoiceReceiver ? personalDetailsList?.[chatReport.invoiceReceiver.accountID] : null;
-    const [iouReport, transactions] = useReportWithTransactionsAndViolations(iouReportID);
+    const reportTransactionsCollection = useReportTransactionsCollection(iouReportID);
+    const {isOffline} = useNetwork();
+    const transactions = useMemo(
+        () =>
+            Object.values(reportTransactionsCollection ?? {}).filter(
+                (transaction): transaction is Transaction => !!transaction && (isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
+            ),
+        [reportTransactionsCollection, isOffline],
+    );
     const policy = usePolicy(policyID);
     const lastTransaction = transactions?.at(0);
     const lastTransactionViolations = useTransactionViolations(lastTransaction?.transactionID);
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts
index 95eee088dd10..24ebfd88f587 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts
@@ -39,6 +39,9 @@ type MoneyRequestReportPreviewProps = {
     /** The active IOUReport, used for Onyx subscription */
     iouReportID: string | undefined;
 
+    /** The stabilized IOU report, provided by the parent so the preview does not re-subscribe to the churning report */
+    iouReport: OnyxEntry<Report>;
+
     /** Callback when the payment options popover is shown */
     onPaymentOptionsShow?: () => void;
 
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts
new file mode 100644
index 000000000000..a7d00ece15d1
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts
@@ -0,0 +1,203 @@
+import {useEffect, useMemo} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated';
+import useLocalize from '@hooks/useLocalize';
+import {getInvoicePayerName} from '@libs/ReportNameUtils';
+import {getDisplayNameForParticipant, getPolicyName} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import type {PersonalDetails, Policy, Report} from '@src/types/onyx';
+
+type UsePreviewMessageAnimationParams = {
+    /** Whether all the requests are being smart scanned */
+    isScanning: boolean;
+
+    /** Number of pending (managed card) requests */
+    numberOfPendingRequests: number;
+
+    /** Total number of requests */
+    numberOfRequests: number;
+
+    /** Whether the RTER violation message should be shown */
+    shouldShowRTERViolationMessage: boolean;
+
+    /** Whether the chat is a policy expense chat */
+    isPolicyExpenseChat: boolean;
+
+    /** Whether the chat is a trip room */
+    isTripRoom: boolean;
+
+    /** Whether the chat is an invoice room */
+    isInvoiceRoom: boolean;
+
+    /** Whether the report is approved */
+    isApproved: boolean;
+
+    /** Whether the IOU is settled */
+    iouSettled: boolean;
+
+    /** The previewed IOU report */
+    iouReport: OnyxEntry<Report>;
+
+    /** Whether the report has non-reimbursable transactions */
+    hasNonReimbursableTransactions: boolean;
+
+    /** The total amount spent on the report */
+    totalDisplaySpend: number;
+
+    /** The chat report the preview belongs to */
+    chatReport: OnyxEntry<Report>;
+
+    /** The policy the report belongs to */
+    policy: OnyxEntry<Policy>;
+
+    /** The invoice receiver policy, used to resolve the payer name in invoice rooms */
+    invoiceReceiverPolicy: OnyxEntry<Policy>;
+
+    /** The invoice receiver personal detail, used to resolve the payer name in invoice rooms */
+    invoiceReceiverPersonalDetail: OnyxEntry<PersonalDetails> | null;
+
+    /** The manager account ID */
+    managerID: number;
+
+    /** Whether the paid animation is running */
+    isPaidAnimationRunning: boolean;
+
+    /** Whether the approved animation is running */
+    isApprovedAnimationRunning: boolean;
+
+    /** Whether the submitting animation is running */
+    isSubmittingAnimationRunning: boolean;
+};
+
+/**
+ * Owns the preview-message animations: the opacity flash that fires whenever the computed preview message changes,
+ * and the checkmark / thumbs-up spring animations that play on settle / approval. Returns the animated style applied
+ * to the report name container.
+ */
+function usePreviewMessageAnimation({
+    isScanning,
+    numberOfPendingRequests,
+    numberOfRequests,
+    shouldShowRTERViolationMessage,
+    isPolicyExpenseChat,
+    isTripRoom,
+    isInvoiceRoom,
+    isApproved,
+    iouSettled,
+    iouReport,
+    hasNonReimbursableTransactions,
+    totalDisplaySpend,
+    chatReport,
+    policy,
+    invoiceReceiverPolicy,
+    invoiceReceiverPersonalDetail,
+    managerID,
+    isPaidAnimationRunning,
+    isApprovedAnimationRunning,
+    isSubmittingAnimationRunning,
+}: UsePreviewMessageAnimationParams) {
+    const {translate, formatPhoneNumber} = useLocalize();
+
+    const previewMessageOpacity = useSharedValue(1);
+    const previewMessageStyle = useAnimatedStyle(() => ({
+        opacity: previewMessageOpacity.get(),
+    }));
+    const checkMarkScale = useSharedValue(iouSettled ? 1 : 0);
+    const thumbsUpScale = useSharedValue(isApproved ? 1 : 0);
+
+    const previewMessage = useMemo(() => {
+        if (isScanning) {
+            return totalDisplaySpend ? `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('common.scanning')}` : `${translate('common.receipt')}`;
+        }
+        if (numberOfPendingRequests === 1 && numberOfRequests === 1) {
+            return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`;
+        }
+        if (shouldShowRTERViolationMessage) {
+            return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`;
+        }
+
+        let payerOrApproverName;
+        if (isPolicyExpenseChat || isTripRoom) {
+            payerOrApproverName = getPolicyName({report: chatReport, policy});
+        } else if (isInvoiceRoom) {
+            payerOrApproverName = getInvoicePayerName(chatReport, invoiceReceiverPolicy, invoiceReceiverPersonalDetail);
+        } else {
+            payerOrApproverName = getDisplayNameForParticipant({
+                accountID: managerID,
+                shouldUseShortForm: true,
+                formatPhoneNumber,
+            });
+        }
+
+        if (isApproved) {
+            return translate('iou.managerApproved', payerOrApproverName);
+        }
+        let paymentVerb: TranslationPaths = 'iou.payerOwes';
+        if (iouSettled || iouReport?.isWaitingOnBankAccount) {
+            paymentVerb = 'iou.payerPaid';
+        } else if (hasNonReimbursableTransactions) {
+            paymentVerb = 'iou.payerSpent';
+            payerOrApproverName = getDisplayNameForParticipant({
+                accountID: chatReport?.ownerAccountID,
+                shouldUseShortForm: true,
+                formatPhoneNumber,
+            });
+        }
+        return translate(paymentVerb, payerOrApproverName);
+    }, [
+        isScanning,
+        numberOfPendingRequests,
+        numberOfRequests,
+        shouldShowRTERViolationMessage,
+        isPolicyExpenseChat,
+        isTripRoom,
+        isInvoiceRoom,
+        isApproved,
+        iouSettled,
+        iouReport?.isWaitingOnBankAccount,
+        hasNonReimbursableTransactions,
+        translate,
+        totalDisplaySpend,
+        chatReport,
+        policy,
+        invoiceReceiverPolicy,
+        invoiceReceiverPersonalDetail,
+        managerID,
+        formatPhoneNumber,
+    ]);
+
+    useEffect(() => {
+        if (!isPaidAnimationRunning || isApprovedAnimationRunning || isSubmittingAnimationRunning) {
+            return;
+        }
+
+        previewMessageOpacity.set(
+            withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => {
+                previewMessageOpacity.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}));
+            }),
+        );
+        // We only want to animate the text when the text changes
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [previewMessage, previewMessageOpacity]);
+
+    useEffect(() => {
+        if (!iouSettled) {
+            return;
+        }
+
+        checkMarkScale.set(isPaidAnimationRunning ? withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})) : 1);
+    }, [isPaidAnimationRunning, iouSettled, checkMarkScale]);
+
+    useEffect(() => {
+        if (!isApproved) {
+            return;
+        }
+
+        thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBS_UP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBS_UP_DURATION})) : 1);
+    }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]);
+
+    return {previewMessageStyle};
+}
+
+export default usePreviewMessageAnimation;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx
new file mode 100644
index 000000000000..877524c72b4d
--- /dev/null
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx
@@ -0,0 +1,281 @@
+import type {FlashListRef, ListRenderItem, ListRenderItemInfo} from '@shopify/flash-list';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {View} from 'react-native';
+import type {ViewToken} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import Text from '@components/Text';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useIsFocusedRef from '@hooks/useIsFocusedRef';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import shouldAdjustScroll from '@libs/shouldAdjustScroll';
+import {compareByRBR} from '@libs/TransactionPreviewUtils';
+import {getCreated} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
+import type {Policy, Report, Transaction} from '@src/types/onyx';
+import type {MoneyRequestReportPreviewStyleType} from './types';
+
+const MAX_PREVIEWS_NUMBER = 10;
+
+const ITEM_LAYOUT_TYPE = {
+    PREVIEW: 'preview',
+    SHOW_MORE: 'showMore',
+};
+
+type UseReportPreviewCarouselParams = {
+    /** Transactions that belong to the previewed report */
+    transactions: Transaction[];
+
+    /** Violations for the previewed transactions, used to sort RBR transactions first */
+    transactionViolations: Parameters<typeof compareByRBR>[2];
+
+    /** The previewed IOU report */
+    iouReport: OnyxEntry<Report>;
+
+    /** The policy the previewed report belongs to */
+    policy: OnyxEntry<Policy>;
+
+    /** Whether the access placeholder is shown (no carousel transactions in that case) */
+    shouldShowAccessPlaceHolder: boolean;
+
+    /** Computed preview styles, used to size the carousel items */
+    reportPreviewStyles: MoneyRequestReportPreviewStyleType;
+
+    /** The measured width of the preview */
+    currentWidth: number;
+
+    /** IDs of newly added transactions, used to scroll them into view */
+    newTransactionIDs?: Set<string>;
+
+    /** Renders a single transaction preview item */
+    renderTransactionItem: ListRenderItem<Transaction>;
+};
+
+/**
+ * Owns all transaction-carousel state, refs, effects and derived values for the money request report preview.
+ * The shell passes the returned list props to the carousel body and the arrow controls to the header.
+ */
+function useReportPreviewCarousel({
+    transactions,
+    transactionViolations,
+    iouReport,
+    policy,
+    shouldShowAccessPlaceHolder,
+    reportPreviewStyles,
+    currentWidth,
+    newTransactionIDs,
+    renderTransactionItem,
+}: UseReportPreviewCarouselParams) {
+    const styles = useThemeStyles();
+    const {translate, localeCompare} = useLocalize();
+    const currentUserDetails = useCurrentUserPersonalDetails();
+    const isFocusedRef = useIsFocusedRef();
+
+    const carouselTransactions = useMemo(() => {
+        if (shouldShowAccessPlaceHolder) {
+            return [];
+        }
+        const sorted = [...transactions].sort((a, b) => {
+            const rbrComparison = compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy);
+            if (rbrComparison !== 0) {
+                return rbrComparison;
+            }
+            // Tiebreak by date (ascending — oldest first) so position is stable across RBR state changes
+            return localeCompare(getCreated(a), getCreated(b));
+        });
+        return sorted.slice(0, MAX_PREVIEWS_NUMBER + 1);
+    }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy, localeCompare]);
+    const prevCarouselTransactionLength = useRef(0);
+
+    useEffect(() => {
+        return () => {
+            prevCarouselTransactionLength.current = carouselTransactions.length;
+        };
+    }, [carouselTransactions.length]);
+
+    const [currentIndex, setCurrentIndex] = useState(0);
+    const [currentVisibleItems, setCurrentVisibleItems] = useState([0]);
+    const [footerWidth, setFooterWidth] = useState(0);
+    // optimisticIndex - value for index we are scrolling to with an arrow button or undefined after scroll is completed
+    // value ensures that disabled state is applied instantly and not overridden by onViewableItemsChanged when scrolling
+    // undefined makes arrow buttons react on currentIndex changes when scrolling manually
+    const [optimisticIndex, setOptimisticIndex] = useState<number | undefined>(undefined);
+    const carouselRef = useRef<FlashListRef<Transaction> | null>(null);
+    // Expose a callback ref instead of the ref object so the ref does not flow through the hook's return value
+    // (React Compiler forbids reading/passing refs during render).
+    const setCarouselRef = useCallback((node: FlashListRef<Transaction> | null) => {
+        carouselRef.current = node;
+    }, []);
+    const prevTransactionCountForScroll = useRef(carouselTransactions.length);
+    const [carouselKey, setCarouselKey] = useState(0);
+
+    // Reset carousel when transitioning from empty to non-empty data.
+    // scrollToOffset doesn't clear RecyclerListView's internal layout cache on iOS mobile web,
+    // so we force a full re-mount via key to prevent new items from rendering off-screen.
+    useEffect(() => {
+        if (carouselTransactions.length > 0 && prevTransactionCountForScroll.current === 0) {
+            setCurrentIndex(0);
+            setOptimisticIndex(undefined);
+            setCarouselKey((prev) => prev + 1);
+        }
+        prevTransactionCountForScroll.current = carouselTransactions.length;
+    }, [carouselTransactions.length]);
+
+    const visibleItemsOnEndCount = useMemo(() => {
+        const lastItemWidth = transactions.length > MAX_PREVIEWS_NUMBER ? footerWidth : reportPreviewStyles.transactionPreviewCarouselStyle.width;
+        const lastItemWithGap = lastItemWidth + styles.gap2.gap;
+        const itemWithGap = reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.gap2.gap;
+        return Math.floor((currentWidth - 2 * styles.pl2.paddingLeft - lastItemWithGap) / itemWithGap) + 1;
+    }, [transactions.length, footerWidth, reportPreviewStyles.transactionPreviewCarouselStyle.width, styles.gap2.gap, styles.pl2.paddingLeft, currentWidth]);
+    const viewabilityConfig = useMemo(() => {
+        return {itemVisiblePercentThreshold: 100};
+    }, []);
+
+    const carouselTransactionsRef = useRef(carouselTransactions);
+
+    useEffect(() => {
+        carouselTransactionsRef.current = carouselTransactions;
+    }, [carouselTransactions]);
+
+    useEffect(() => {
+        const index = carouselTransactions.findIndex((transaction) => newTransactionIDs?.has(transaction.transactionID));
+
+        if (index < 0) {
+            return;
+        }
+        const newTransaction = carouselTransactions.at(index);
+        setTimeout(() => {
+            if (!isFocusedRef.current) {
+                return;
+            }
+
+            // If the new transaction is not available at the index it was on before the delay, avoid the scrolling
+            // because we are scrolling to either a wrong or unavailable transaction (which can cause crash).
+            if (newTransaction?.transactionID !== carouselTransactionsRef.current.at(index)?.transactionID) {
+                return;
+            }
+
+            carouselRef.current?.scrollToIndex({
+                index,
+                viewOffset: -2 * styles.gap2.gap,
+                animated: true,
+            });
+        }, CONST.PENDING_TRANSACTION_SCROLL_DELAY);
+
+        // We only want to scroll to a new transaction when the set of new transaction IDs changes.
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [newTransactionIDs]);
+
+    const onViewableItemsChanged = useCallback(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
+        const newIndex = viewableItems.at(0)?.index;
+        if (typeof newIndex === 'number') {
+            setCurrentIndex(newIndex);
+        }
+        const viewableItemsIndexes = viewableItems.map((item) => item.index).filter((item): item is number => item !== null);
+        setCurrentVisibleItems(viewableItemsIndexes);
+    }, []);
+
+    const snapOffsets = carouselTransactions.map((_, index) => index * (reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.transactionsCarouselGap.width));
+
+    const handleChange = (index: number) => {
+        if (index > carouselTransactions.length - visibleItemsOnEndCount) {
+            const lastScrollableIndex = carouselTransactions.length - visibleItemsOnEndCount;
+            setOptimisticIndex(lastScrollableIndex);
+            carouselRef.current?.scrollToOffset({
+                offset: snapOffsets.at(lastScrollableIndex) ?? 0,
+                animated: true,
+            });
+            return;
+        }
+        if (index < 0) {
+            setOptimisticIndex(0);
+            carouselRef.current?.scrollToTop({animated: true});
+            return;
+        }
+        if (index === carouselTransactions.length - visibleItemsOnEndCount) {
+            setOptimisticIndex(index);
+            carouselRef.current?.scrollToEnd({animated: true});
+            return;
+        }
+        setOptimisticIndex(index);
+        carouselRef.current?.scrollToOffset({
+            offset: snapOffsets.at(index) ?? 0,
+            animated: true,
+        });
+    };
+
+    const renderItem = (itemInfo: ListRenderItemInfo<Transaction>) => {
+        if (itemInfo.index > MAX_PREVIEWS_NUMBER - 1) {
+            return (
+                <View
+                    style={[styles.p5, styles.justifyContentCenter]}
+                    onLayout={(e) => setFooterWidth(e.nativeEvent.layout.width)}
+                >
+                    <Text style={styles.textBlue}>
+                        +{transactions.length - MAX_PREVIEWS_NUMBER} {translate('common.more').toLowerCase()}
+                    </Text>
+                </View>
+            );
+        }
+        return renderTransactionItem(itemInfo);
+    };
+
+    useEffect(() => {
+        if (
+            optimisticIndex === undefined ||
+            optimisticIndex !== currentIndex ||
+            // currentIndex is still the same as target (f.ex. 0), but not yet scrolled to the far left
+            (currentVisibleItems.at(0) !== optimisticIndex && optimisticIndex !== undefined) ||
+            // currentIndex reached, but not scrolled to the end
+            (optimisticIndex === carouselTransactions.length - visibleItemsOnEndCount && currentVisibleItems.length !== visibleItemsOnEndCount)
+        ) {
+            return;
+        }
+        // Clears the transient optimistic index once the FlashList scroll (an external system) catches up to the
+        // arrow-driven target, so the arrows fall back to reacting to the real scroll position.
+        // eslint-disable-next-line react-hooks/set-state-in-effect
+        setOptimisticIndex(undefined);
+    }, [carouselTransactions.length, currentIndex, currentVisibleItems, currentVisibleItems.length, optimisticIndex, visibleItemsOnEndCount]);
+
+    const adjustScroll = useCallback(() => {
+        // Workaround for a known React Native bug on Android (https://github.com/facebook/react-native/issues/27504):
+        // When the FlatList is scrolled to the end and the last item is deleted, a blank space is left behind.
+        // To fix this, we detect when onEndReached is triggered due to an item deletion,
+        // and programmatically scroll to the end to fill the space.
+        if (carouselTransactions.length >= prevCarouselTransactionLength.current || !shouldAdjustScroll) {
+            return;
+        }
+        prevCarouselTransactionLength.current = carouselTransactions.length;
+        carouselRef.current?.scrollToEnd();
+    }, [carouselTransactions.length]);
+
+    const renderSeparator = () => <View style={styles.transactionsCarouselGap} />;
+
+    const getItemType = (_item: Transaction, index: number) => {
+        return index === MAX_PREVIEWS_NUMBER ? ITEM_LAYOUT_TYPE.SHOW_MORE : ITEM_LAYOUT_TYPE.PREVIEW;
+    };
+
+    const goToPrevious = () => handleChange(currentIndex - 1);
+    const goToNext = () => handleChange(currentIndex + 1);
+    const isPreviousDisabled = optimisticIndex !== undefined ? optimisticIndex === 0 : currentIndex === 0 && currentVisibleItems.at(0) === 0;
+    const isNextDisabled = optimisticIndex ? optimisticIndex + visibleItemsOnEndCount >= carouselTransactions.length : currentVisibleItems.at(-1) === carouselTransactions.length - 1;
+
+    return {
+        carouselTransactions,
+        carouselKey,
+        setCarouselRef,
+        snapOffsets,
+        renderItem,
+        getItemType,
+        renderSeparator,
+        viewabilityConfig,
+        onViewableItemsChanged,
+        adjustScroll,
+        goToPrevious,
+        goToNext,
+        isPreviousDisabled,
+        isNextDisabled,
+    };
+}
+
+export default useReportPreviewCarousel;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ac9fc3239da6..59131edf0587 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -2641,20 +2641,6 @@ function isPolicyAdmin(policy: OnyxEntry<Policy>): boolean {
     return policyRole === CONST.POLICY.ROLE.ADMIN;
 }
 
-/**
- * Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes
- */
-function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): boolean {
-    const transactions = getReportTransactions(iouReportID);
-
-    // Early return false in case not having any transaction
-    if (!transactions || transactions.length === 0) {
-        return false;
-    }
-
-    return transactions.every((transaction) => isFetchingWaypointsFromServer(transaction));
-}
-
 /**
  * If the report is a thread and has a chat type set, it is a expense chat.
  */
@@ -13317,7 +13303,6 @@ export {
     hasIOUWaitingOnCurrentUserBankAccount,
     hasNonReimbursableTransactions,
     hasOnlyHeldExpenses,
-    hasOnlyTransactionsWithPendingRoutes,
     hasReceiptError,
     hasReportNameError,
     getReportActionWithSmartscanError,
diff --git a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx
index 08b02c52fe45..de50c02018b5 100644
--- a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx
+++ b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx
@@ -197,6 +197,7 @@ function ActionContentRouter({
         return (
             <MoneyRequestReportPreview
                 iouReportID={getIOUReportIDFromReportActionPreview(action)}
+                iouReport={iouReport}
                 policyID={policyID}
                 chatReportID={reportID}
                 chatReport={report}
diff --git a/tests/ui/MoneyRequestReportPreview.test.tsx b/tests/ui/MoneyRequestReportPreview.test.tsx
index 84269f259f4b..deecd31ae6d1 100644
--- a/tests/ui/MoneyRequestReportPreview.test.tsx
+++ b/tests/ui/MoneyRequestReportPreview.test.tsx
@@ -24,6 +24,7 @@ import * as ReportUtils from '@src/libs/ReportUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Report, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
 import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
 import {actionR14932 as mockAction} from '../../__mocks__/reportData/actions';
 import {chatReportR14932 as mockChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports';
 import {transactionR14932 as mockTransaction} from '../../__mocks__/reportData/transactions';
@@ -68,6 +69,17 @@ jest.mock('@src/hooks/useReportWithTransactionsAndViolations', () => ({
     default: (...args: Parameters<typeof mockUseReportWithTransactionsAndViolations>) => mockUseReportWithTransactionsAndViolations(...args),
 }));
 
+// The preview reads `iouReport` from a prop (provided stable by the parent) and its transactions from the
+// scoped `useReportTransactionsCollection` hook, so the test drives those two sources directly.
+let mockIOUReportProp: OnyxEntry<Report> = mockIOUReport;
+
+const mockUseReportTransactionsCollection = jest.fn(() => toCollectionDataSet(ONYXKEYS.COLLECTION.TRANSACTION, defaultPreviewTransactions, (transaction) => transaction.transactionID));
+
+jest.mock('@hooks/useReportTransactionsCollection', () => ({
+    __esModule: true,
+    default: () => mockUseReportTransactionsCollection(),
+}));
+
 type OnHoldMenuOpen = (requestType: string, paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => void;
 
 // Capture the onHoldMenuOpen callback the preview passes to the pay button so a held-expense payment can be triggered
@@ -141,6 +153,7 @@ const renderPage = ({isWhisper = false, isHovered = false}: Partial<MoneyRequest
                                     policyID={mockChatReport.policyID}
                                     action={mockAction}
                                     iouReportID={mockIOUReport.reportID}
+                                    iouReport={mockIOUReportProp}
                                     chatReportID={mockChatReport.reportID}
                                     chatReport={mockChatReport}
                                     onPaymentOptionsShow={() => {}}
@@ -198,17 +211,15 @@ const setReportPreviewData = (
     overrides: {
         iouReport?: OnyxEntry<Report>;
         transactions?: Transaction[];
-        violations?: OnyxCollection<TransactionViolation[]>;
     } = {},
 ) => {
-    const {iouReport, transactions, violations} = overrides;
+    const {iouReport, transactions} = overrides;
     const hasIouReportOverride = Object.prototype.hasOwnProperty.call(overrides, 'iouReport');
 
-    mockUseReportWithTransactionsAndViolations.mockImplementation(() => [
-        hasIouReportOverride ? iouReport : mockIOUReport,
-        transactions ?? defaultPreviewTransactions,
-        violations ?? {violations: mockViolations},
-    ]);
+    mockIOUReportProp = hasIouReportOverride ? iouReport : mockIOUReport;
+    mockUseReportTransactionsCollection.mockImplementation(() =>
+        toCollectionDataSet(ONYXKEYS.COLLECTION.TRANSACTION, transactions ?? defaultPreviewTransactions, (transaction) => transaction.transactionID),
+    );
 };
 
 const setHasOnceLoadedReportActions = async (hasOnceLoadedReportActions: boolean) => {
