From 037b3c5557cd155843fe1a5bb8c25232c1476796 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 1 Jul 2025 11:36:34 +0200
Subject: [PATCH 1/7] fix PR comments

---
 Mobile-Expensify                              |  2 +-
 src/components/Modal/BaseModal.tsx            | 14 +++-----------
 src/components/Modal/ReanimatedModal/utils.ts |  6 ++++--
 3 files changed, 8 insertions(+), 14 deletions(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index a3e8c279f4d2..dc4e3a0644e4 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit a3e8c279f4d2b303ed18f32fdc88f2b88af4f02d
+Subproject commit dc4e3a0644e4663ad2d8c9fc20f217033ec2601f
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index f01ed4ba6ede..8fcd4d25975c 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -30,7 +30,6 @@ import type ReanimatedModalProps from './ReanimatedModal/types';
 import type BaseModalProps from './types';
 
 const REANIMATED_MODAL_TYPES: Array<ValueOf<typeof CONST.MODAL.MODAL_TYPE>> = [CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED, CONST.MODAL.MODAL_TYPE.FULLSCREEN];
-const PARTIAL_REANIMATED_MODAL_TYPES: Array<ValueOf<typeof CONST.MODAL.MODAL_TYPE>> = [CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE];
 
 type ModalComponentProps = (ReactNativeModalProps | ReanimatedModalProps) & {
     type?: ValueOf<typeof CONST.MODAL.MODAL_TYPE>;
@@ -38,16 +37,8 @@ type ModalComponentProps = (ReactNativeModalProps | ReanimatedModalProps) & {
 };
 
 function ModalComponent({type, shouldUseReanimatedModal, ...props}: ModalComponentProps) {
-    if (type && REANIMATED_MODAL_TYPES.includes(type)) {
-        return (
-            <ReanimatedModal
-                // eslint-disable-next-line react/jsx-props-no-spreading
-                {...(props as ReanimatedModalProps)}
-                type={type}
-            />
-        );
-    }
-    if (type && PARTIAL_REANIMATED_MODAL_TYPES.includes(type) && shouldUseReanimatedModal) {
+    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+    if ((type && REANIMATED_MODAL_TYPES.includes(type)) || shouldUseReanimatedModal) {
         return (
             <ReanimatedModal
                 // eslint-disable-next-line react/jsx-props-no-spreading
@@ -56,6 +47,7 @@ function ModalComponent({type, shouldUseReanimatedModal, ...props}: ModalCompone
             />
         );
     }
+
     // eslint-disable-next-line react/jsx-props-no-spreading
     return <ReactNativeModal {...(props as ReactNativeModalProps)} />;
 }
diff --git a/src/components/Modal/ReanimatedModal/utils.ts b/src/components/Modal/ReanimatedModal/utils.ts
index eb9feb3f9054..5442c534882c 100644
--- a/src/components/Modal/ReanimatedModal/utils.ts
+++ b/src/components/Modal/ReanimatedModal/utils.ts
@@ -23,7 +23,6 @@ function getModalInAnimation(animationType: AnimationInType): ValidKeyframeProps
                 },
             };
         case 'fadeIn':
-        default:
             return {
                 from: {opacity: 0},
                 to: {
@@ -31,6 +30,8 @@ function getModalInAnimation(animationType: AnimationInType): ValidKeyframeProps
                     easing,
                 },
             };
+        default:
+            throw new Error('Unknown animation type');
     }
 }
 
@@ -53,7 +54,6 @@ function getModalOutAnimation(animationType: AnimationOutType): ValidKeyframePro
                 },
             };
         case 'fadeOut':
-        default:
             return {
                 from: {opacity: 0.72},
                 to: {
@@ -61,6 +61,8 @@ function getModalOutAnimation(animationType: AnimationOutType): ValidKeyframePro
                     easing,
                 },
             };
+        default:
+            throw new Error('Unknown animation type');
     }
 }
 

From 470e4b9ba61e1b182f84e1f1549723509d79dcd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 1 Jul 2025 14:29:17 +0200
Subject: [PATCH 2/7] add swipability to reanimated modal, use reanimated modal
 in search router, fix search router swipe

---
 .../Container/GestureHandler.tsx              |  76 +++++++++++
 .../Modal/ReanimatedModal/Container/index.tsx |  22 +++-
 .../Modal/ReanimatedModal/index.tsx           |   5 +
 src/components/Modal/ReanimatedModal/types.ts |  18 ++-
 .../Search/SearchRouter/SearchRouter.tsx      | 120 ++++++++++--------
 .../Search/SearchRouter/SearchRouterModal.tsx |   1 +
 6 files changed, 179 insertions(+), 63 deletions(-)
 create mode 100644 src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx

diff --git a/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx b/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx
new file mode 100644
index 000000000000..426fc95bcaae
--- /dev/null
+++ b/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx
@@ -0,0 +1,76 @@
+import type {PropsWithChildren} from 'react';
+import React, {useMemo} from 'react';
+import type {GestureStateChangeEvent, GestureType, PanGestureHandlerEventPayload} from 'react-native-gesture-handler';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import {runOnJS, useSharedValue} from 'react-native-reanimated';
+import type {GestureHandlerProps, SwipeDirection} from '@components/Modal/ReanimatedModal/types';
+import CONST from '@src/CONST';
+
+function hasSwipeEnded(
+    e: GestureStateChangeEvent<PanGestureHandlerEventPayload>,
+    initialPosition: {x: number; y: number},
+    swipeThreshold: number,
+    swipeDirection?: SwipeDirection | SwipeDirection[],
+    onSwipeComplete?: () => void,
+) {
+    'worklet';
+
+    if (!swipeDirection || !onSwipeComplete) {
+        return;
+    }
+    const directions = Array.isArray(swipeDirection) ? swipeDirection : [swipeDirection];
+
+    for (const direction of directions) {
+        switch (direction) {
+            case CONST.SWIPE_DIRECTION.RIGHT:
+                if (e.translationX - initialPosition.x > swipeThreshold) {
+                    runOnJS(onSwipeComplete)();
+                }
+                break;
+            case CONST.SWIPE_DIRECTION.LEFT:
+                if (initialPosition.x - e.translationX > swipeThreshold) {
+                    runOnJS(onSwipeComplete)();
+                }
+                break;
+            case CONST.SWIPE_DIRECTION.UP:
+                if (initialPosition.y - e.translationY > swipeThreshold) {
+                    runOnJS(onSwipeComplete)();
+                }
+                break;
+            case CONST.SWIPE_DIRECTION.DOWN:
+                if (e.translationY - initialPosition.y > swipeThreshold) {
+                    runOnJS(onSwipeComplete)();
+                }
+                break;
+            default:
+                throw new Error('Unknown swipe direction');
+        }
+    }
+}
+
+function GestureHandler({swipeDirection, onSwipeComplete, swipeThreshold = 100, children}: PropsWithChildren<GestureHandlerProps>) {
+    const initialTranslationX = useSharedValue(0);
+    const initialTranslationY = useSharedValue(0);
+    const panGesture: GestureType = useMemo(
+        () =>
+            Gesture.Pan()
+                .onStart((e) => {
+                    initialTranslationX.set(e.translationX);
+                    initialTranslationY.set(e.translationY);
+                })
+                .onEnd((e) => {
+                    hasSwipeEnded(e, {x: initialTranslationX.get(), y: initialTranslationY.get()}, swipeThreshold, swipeDirection, onSwipeComplete);
+                }),
+        [initialTranslationX, initialTranslationY, onSwipeComplete, swipeDirection, swipeThreshold],
+    );
+
+    if (!swipeDirection || !onSwipeComplete) {
+        return children;
+    }
+
+    return <GestureDetector gesture={panGesture}>{children}</GestureDetector>;
+}
+
+GestureHandler.displayName = 'GestureHandler';
+
+export default GestureHandler;
diff --git a/src/components/Modal/ReanimatedModal/Container/index.tsx b/src/components/Modal/ReanimatedModal/Container/index.tsx
index 1ab545e50c6b..d9c50b8d9b6d 100644
--- a/src/components/Modal/ReanimatedModal/Container/index.tsx
+++ b/src/components/Modal/ReanimatedModal/Container/index.tsx
@@ -6,6 +6,7 @@ import type {ContainerProps} from '@components/Modal/ReanimatedModal/types';
 import {getModalInAnimation, getModalOutAnimation} from '@components/Modal/ReanimatedModal/utils';
 import useThemeStyles from '@hooks/useThemeStyles';
 import CONST from '@src/CONST';
+import GestureHandler from './GestureHandler';
 
 function Container({
     style,
@@ -16,6 +17,9 @@ function Container({
     animationIn,
     animationOut,
     type,
+    onSwipeComplete,
+    swipeDirection,
+    swipeThreshold = 100,
     ...props
 }: Partial<ReanimatedModalProps> & ContainerProps) {
     const styles = useThemeStyles();
@@ -46,13 +50,19 @@ function Container({
             // eslint-disable-next-line react/jsx-props-no-spreading
             {...props}
         >
-            <Animated.View
-                style={[styles.modalAnimatedContainer, type !== CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED && styles.flex1]}
-                entering={Entering}
-                exiting={Exiting}
+            <GestureHandler
+                swipeThreshold={swipeThreshold}
+                swipeDirection={swipeDirection}
+                onSwipeComplete={onSwipeComplete}
             >
-                {props.children}
-            </Animated.View>
+                <Animated.View
+                    style={[styles.modalAnimatedContainer, type !== CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED && styles.flex1]}
+                    entering={Entering}
+                    exiting={Exiting}
+                >
+                    {props.children}
+                </Animated.View>
+            </GestureHandler>
         </View>
     );
 }
diff --git a/src/components/Modal/ReanimatedModal/index.tsx b/src/components/Modal/ReanimatedModal/index.tsx
index ab873254fc29..db0c3b8b22c6 100644
--- a/src/components/Modal/ReanimatedModal/index.tsx
+++ b/src/components/Modal/ReanimatedModal/index.tsx
@@ -37,6 +37,9 @@ function ReanimatedModal({
     style,
     type,
     statusBarTranslucent = false,
+    onSwipeComplete,
+    swipeDirection,
+    swipeThreshold,
     ...props
 }: ReanimatedModalProps) {
     const [isVisibleState, setIsVisibleState] = useState(isVisible);
@@ -148,6 +151,8 @@ function ReanimatedModal({
             animationOut={animationOut as AnimationOutType}
             style={style}
             type={type}
+            onSwipeComplete={onSwipeComplete}
+            swipeDirection={swipeDirection}
         >
             {children}
         </Container>
diff --git a/src/components/Modal/ReanimatedModal/types.ts b/src/components/Modal/ReanimatedModal/types.ts
index cf03deb66340..dbcc19ae82df 100644
--- a/src/components/Modal/ReanimatedModal/types.ts
+++ b/src/components/Modal/ReanimatedModal/types.ts
@@ -13,13 +13,27 @@ type GestureProps = {
     deviceWidth?: number | null;
 };
 
+type SwipeDirection = ValueOf<typeof CONST.SWIPE_DIRECTION>;
+
+type GestureHandlerProps = {
+    /** Callback to be fired on swipe gesture. */
+    onSwipeComplete?: () => void;
+
+    /** Threshold for swipe gesture. */
+    swipeThreshold: number;
+
+    /** Threshold for swipe gesture. */
+    swipeDirection?: SwipeDirection | SwipeDirection[];
+};
+
 type AnimationInType = 'fadeIn' | 'slideInUp' | 'slideInRight';
 type AnimationOutType = 'fadeOut' | 'slideOutDown' | 'slideOutRight';
 
 type AnimationOut = ValueOf<Pick<ReactNativeModalProps, 'animationOut'>>;
 
 type ReanimatedModalProps = ViewProps &
-    GestureProps & {
+    GestureProps &
+    GestureHandlerProps & {
         /** Content inside the modal */
         children: ReactNode;
 
@@ -168,4 +182,4 @@ type ContainerProps = {
 };
 
 export default ReanimatedModalProps;
-export type {BackdropProps, ContainerProps, AnimationOut, AnimationInType, AnimationOutType};
+export type {BackdropProps, ContainerProps, GestureHandlerProps, AnimationOut, AnimationInType, AnimationOutType, SwipeDirection};
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 543c7e3f8045..3fa852d6db79 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -3,7 +3,9 @@ import {deepEqual} from 'fast-equals';
 import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
 import type {TextInputProps} from 'react-native';
 import {InteractionManager, Keyboard, View} from 'react-native';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
 import {useOnyx} from 'react-native-onyx';
+import {runOnJS} from 'react-native-reanimated';
 import type {ValueOf} from 'type-fest';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -318,63 +320,71 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
     const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
     const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata);
 
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const tap = Gesture.Tap().onFinalize(() => {
+        'worklet';
+
+        runOnJS(() => {
+            Keyboard.dismiss();
+        })();
+    });
     return (
-        <View
-            style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
-            testID={SearchRouter.displayName}
-            ref={ref}
-            onStartShouldSetResponder={() => true}
-            onResponderRelease={Keyboard.dismiss}
-        >
-            {shouldUseNarrowLayout && (
-                <HeaderWithBackButton
-                    title={translate('common.search')}
-                    onBackButtonPress={() => onRouterClose()}
-                    shouldDisplayHelpButton={false}
-                />
-            )}
-            {isRecentSearchesDataLoaded && (
-                <>
-                    <SearchInputSelectionWrapper
-                        value={textInputValue}
-                        isFullWidth={shouldUseNarrowLayout}
-                        onSearchQueryChange={onSearchQueryChange}
-                        onSubmit={() => {
-                            const focusedOption = listRef.current?.getFocusedOption();
-
-                            if (!focusedOption) {
-                                submitSearch(textInputValue);
-                                return;
-                            }
-
-                            onListItemPress(focusedOption);
-                        }}
-                        caretHidden={shouldHideInputCaret}
-                        autocompleteListRef={listRef}
-                        shouldShowOfflineMessage
-                        wrapperStyle={{...styles.border, ...styles.alignItemsCenter}}
-                        outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
-                        wrapperFocusedStyle={styles.borderColorFocus}
-                        isSearchingForReports={isSearchingForReports}
-                        selection={selection}
-                        substitutionMap={autocompleteSubstitutions}
-                        ref={textInputRef}
-                    />
-                    <SearchAutocompleteList
-                        autocompleteQueryValue={autocompleteQueryValue || textInputValue}
-                        handleSearch={searchInServer}
-                        searchQueryItem={searchQueryItem}
-                        getAdditionalSections={getAdditionalSections}
-                        onListItemPress={onListItemPress}
-                        setTextQuery={setTextAndUpdateSelection}
-                        updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
-                        onHighlightFirstItem={() => listRef.current?.updateAndScrollToFocusedIndex(1)}
-                        ref={listRef}
-                        textInputRef={textInputRef}
+        <GestureDetector gesture={tap}>
+            <View
+                style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
+                testID={SearchRouter.displayName}
+                ref={ref}
+            >
+                {shouldUseNarrowLayout && (
+                    <HeaderWithBackButton
+                        title={translate('common.search')}
+                        onBackButtonPress={() => onRouterClose()}
+                        shouldDisplayHelpButton={false}
                     />
-                </>
-            )}
-        </View>
+                )}
+                {isRecentSearchesDataLoaded && (
+                    <>
+                        <SearchInputSelectionWrapper
+                            value={textInputValue}
+                            isFullWidth={shouldUseNarrowLayout}
+                            onSearchQueryChange={onSearchQueryChange}
+                            onSubmit={() => {
+                                const focusedOption = listRef.current?.getFocusedOption();
+
+                                if (!focusedOption) {
+                                    submitSearch(textInputValue);
+                                    return;
+                                }
+
+                                onListItemPress(focusedOption);
+                            }}
+                            caretHidden={shouldHideInputCaret}
+                            autocompleteListRef={listRef}
+                            shouldShowOfflineMessage
+                            wrapperStyle={{...styles.border, ...styles.alignItemsCenter}}
+                            outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
+                            wrapperFocusedStyle={styles.borderColorFocus}
+                            isSearchingForReports={isSearchingForReports}
+                            selection={selection}
+                            substitutionMap={autocompleteSubstitutions}
+                            ref={textInputRef}
+                        />
+                        <SearchAutocompleteList
+                            autocompleteQueryValue={autocompleteQueryValue || textInputValue}
+                            handleSearch={searchInServer}
+                            searchQueryItem={searchQueryItem}
+                            getAdditionalSections={getAdditionalSections}
+                            onListItemPress={onListItemPress}
+                            setTextQuery={setTextAndUpdateSelection}
+                            updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
+                            onHighlightFirstItem={() => listRef.current?.updateAndScrollToFocusedIndex(1)}
+                            ref={listRef}
+                            textInputRef={textInputRef}
+                        />
+                    </>
+                )}
+            </View>
+        </GestureDetector>
     );
 }
 
diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx
index c4a6b5d128bf..232b1bf2fb53 100644
--- a/src/components/Search/SearchRouter/SearchRouterModal.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx
@@ -35,6 +35,7 @@ function SearchRouterModal() {
             onModalShow={() => setShouldHideInputCaret(false)}
             shouldApplySidePanelOffset={!shouldUseNarrowLayout}
             enableEdgeToEdgeBottomSafeAreaPadding
+            shouldUseReanimatedModal
         >
             <ScreenWrapperContainer
                 testID={SearchRouterModal.displayName}

From 65a61053d08eb1b47307326713b9500c27153b34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 1 Jul 2025 15:39:57 +0200
Subject: [PATCH 3/7] only enable ReanimatedModal on small screens

---
 src/components/Search/SearchRouter/SearchRouterModal.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx
index 232b1bf2fb53..ef93bb0188f7 100644
--- a/src/components/Search/SearchRouter/SearchRouterModal.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx
@@ -20,7 +20,7 @@ function SearchRouterModal() {
     const [shouldHideInputCaret, setShouldHideInputCaret] = useState(isMobileWebIOS);
 
     const modalType = shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.CENTERED_SWIPEABLE_TO_RIGHT : CONST.MODAL.MODAL_TYPE.POPOVER;
-
+    // For now were only enabling shouldUseReanimatedModal narrow layouts. On wide ones it's a popover and it is not migrated yet.
     return (
         <Modal
             type={modalType}
@@ -35,7 +35,7 @@ function SearchRouterModal() {
             onModalShow={() => setShouldHideInputCaret(false)}
             shouldApplySidePanelOffset={!shouldUseNarrowLayout}
             enableEdgeToEdgeBottomSafeAreaPadding
-            shouldUseReanimatedModal
+            shouldUseReanimatedModal={shouldUseNarrowLayout}
         >
             <ScreenWrapperContainer
                 testID={SearchRouterModal.displayName}

From bbd5c0478fe08edf19a9818d99204c548b3b2334 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 1 Jul 2025 16:17:03 +0200
Subject: [PATCH 4/7] use animation utils for web backdrop

---
 .../ReanimatedModal/Backdrop/index.web.tsx    | 21 ++++---------------
 1 file changed, 4 insertions(+), 17 deletions(-)

diff --git a/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx b/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
index 6341e86862a5..eb16947866d5 100644
--- a/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
+++ b/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
@@ -1,15 +1,14 @@
 import React, {useMemo} from 'react';
 import {View} from 'react-native';
-import Animated, {Easing, Keyframe} from 'react-native-reanimated';
+import Animated, {Keyframe} from 'react-native-reanimated';
 import type {BackdropProps} from '@components/Modal/ReanimatedModal/types';
+import {getModalInAnimation, getModalOutAnimation} from '@components/Modal/ReanimatedModal/utils';
 import {PressableWithoutFeedback} from '@components/Pressable';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import variables from '@styles/variables';
 import CONST from '@src/CONST';
 
-const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0).factory();
-
 function Backdrop({
     style,
     customBackdrop,
@@ -23,25 +22,13 @@ function Backdrop({
     const {translate} = useLocalize();
 
     const Entering = useMemo(() => {
-        const FadeIn = new Keyframe({
-            from: {opacity: 0},
-            to: {
-                opacity: 0.72,
-                easing,
-            },
-        });
+        const FadeIn = new Keyframe(getModalInAnimation('fadeIn'));
 
         return FadeIn.duration(animationInTiming);
     }, [animationInTiming]);
 
     const Exiting = useMemo(() => {
-        const FadeOut = new Keyframe({
-            from: {opacity: 0.72},
-            to: {
-                opacity: 0,
-                easing,
-            },
-        });
+        const FadeOut = new Keyframe(getModalOutAnimation('fadeOut'));
 
         return FadeOut.duration(animationOutTiming);
     }, [animationOutTiming]);

From 30e17afe10317722a8d6d1d1e07e6d047917a063 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Thu, 3 Jul 2025 15:42:05 +0200
Subject: [PATCH 5/7] was crashing otherwise

---
 src/components/Search/SearchRouter/SearchRouter.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 9cfb64277a01..696f3efb376d 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -429,14 +429,16 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
     const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
     const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata);
 
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const dismissKeyboard = () => {
+        Keyboard.dismiss();
+    };
+
     const tap = Gesture.Tap().onFinalize(() => {
         'worklet';
 
-        runOnJS(() => {
-            Keyboard.dismiss();
-        })();
+        runOnJS(dismissKeyboard)();
     });
+
     return (
         <GestureDetector gesture={tap}>
             <View

From c7fa33eb533b50030a7108c6142899208b13abe1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 8 Jul 2025 16:19:16 +0200
Subject: [PATCH 6/7] fix PR comments

---
 .../Container/GestureHandler.tsx              |   6 +-
 .../Search/SearchRouter/SearchRouter.tsx      | 126 ++++++++----------
 2 files changed, 63 insertions(+), 69 deletions(-)

diff --git a/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx b/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx
index 426fc95bcaae..a03aa2582e71 100644
--- a/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx
+++ b/src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx
@@ -15,7 +15,8 @@ function hasSwipeEnded(
 ) {
     'worklet';
 
-    if (!swipeDirection || !onSwipeComplete) {
+    // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
+    if (!swipeDirection || !swipeDirection?.length || !onSwipeComplete) {
         return;
     }
     const directions = Array.isArray(swipeDirection) ? swipeDirection : [swipeDirection];
@@ -64,7 +65,8 @@ function GestureHandler({swipeDirection, onSwipeComplete, swipeThreshold = 100,
         [initialTranslationX, initialTranslationY, onSwipeComplete, swipeDirection, swipeThreshold],
     );
 
-    if (!swipeDirection || !onSwipeComplete) {
+    // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
+    if (!swipeDirection || !swipeDirection?.length || !onSwipeComplete) {
         return children;
     }
 
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index c36106619378..fff9c46b90be 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -3,8 +3,7 @@ import {deepEqual} from 'fast-equals';
 import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
 import type {TextInputProps} from 'react-native';
 import {InteractionManager, Keyboard, View} from 'react-native';
-import {Gesture, GestureDetector} from 'react-native-gesture-handler';
-import {runOnJS} from 'react-native-reanimated';
+import {Gesture, GestureDetector, GestureHandlerRootView} from 'react-native-gesture-handler';
 import type {ValueOf} from 'type-fest';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -429,73 +428,66 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
     const modalWidth = shouldUseNarrowLayout ? styles.w100 : {width: variables.searchRouterPopoverWidth};
     const isRecentSearchesDataLoaded = !isLoadingOnyxValue(recentSearchesMetadata);
 
-    const dismissKeyboard = () => {
-        Keyboard.dismiss();
-    };
-
-    const tap = Gesture.Tap().onFinalize(() => {
-        'worklet';
-
-        runOnJS(dismissKeyboard)();
-    });
-
     return (
-        <GestureDetector gesture={tap}>
-            <View
-                style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
-                testID={SearchRouter.displayName}
-                ref={ref}
-            >
-                {shouldUseNarrowLayout && (
-                    <HeaderWithBackButton
-                        title={translate('common.search')}
-                        onBackButtonPress={() => onRouterClose()}
-                        shouldDisplayHelpButton={false}
-                    />
-                )}
-                {isRecentSearchesDataLoaded && (
-                    <>
-                        <SearchInputSelectionWrapper
-                            value={textInputValue}
-                            isFullWidth={shouldUseNarrowLayout}
-                            onSearchQueryChange={onSearchQueryChange}
-                            onSubmit={() => {
-                                const focusedOption = listRef.current?.getFocusedOption();
-
-                                if (!focusedOption) {
-                                    submitSearch(textInputValue);
-                                    return;
-                                }
-
-                                onListItemPress(focusedOption);
-                            }}
-                            caretHidden={shouldHideInputCaret}
-                            autocompleteListRef={listRef}
-                            shouldShowOfflineMessage
-                            wrapperStyle={{...styles.border, ...styles.alignItemsCenter}}
-                            outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
-                            wrapperFocusedStyle={styles.borderColorFocus}
-                            isSearchingForReports={isSearchingForReports}
-                            selection={selection}
-                            substitutionMap={autocompleteSubstitutions}
-                            ref={textInputRef}
-                        />
-                        <SearchAutocompleteList
-                            autocompleteQueryValue={autocompleteQueryValue || textInputValue}
-                            handleSearch={searchInServer}
-                            searchQueryItem={searchQueryItem}
-                            getAdditionalSections={getAdditionalSections}
-                            onListItemPress={onListItemPress}
-                            setTextQuery={setTextAndUpdateSelection}
-                            updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
-                            onHighlightFirstItem={() => listRef.current?.updateAndScrollToFocusedIndex(1)}
-                            ref={listRef}
-                            textInputRef={textInputRef}
+        <GestureHandlerRootView style={{flex: 1}}>
+            <GestureDetector gesture={Gesture.Tap().runOnJS(true).onFinalize(Keyboard.dismiss)}>
+                {' '}
+                <View
+                    style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
+                    testID={SearchRouter.displayName}
+                    ref={ref}
+                >
+                    {shouldUseNarrowLayout && (
+                        <HeaderWithBackButton
+                            title={translate('common.search')}
+                            onBackButtonPress={() => onRouterClose()}
+                            shouldDisplayHelpButton={false}
                         />
-                    </>
-                )}
-            </View>
-        </GestureDetector>
+                    )}
+                    {isRecentSearchesDataLoaded && (
+                        <>
+                            <SearchInputSelectionWrapper
+                                value={textInputValue}
+                                isFullWidth={shouldUseNarrowLayout}
+                                onSearchQueryChange={onSearchQueryChange}
+                                onSubmit={() => {
+                                    const focusedOption = listRef.current?.getFocusedOption();
+
+                                    if (!focusedOption) {
+                                        submitSearch(textInputValue);
+                                        return;
+                                    }
+
+                                    onListItemPress(focusedOption);
+                                }}
+                                caretHidden={shouldHideInputCaret}
+                                autocompleteListRef={listRef}
+                                shouldShowOfflineMessage
+                                wrapperStyle={{...styles.border, ...styles.alignItemsCenter}}
+                                outerWrapperStyle={[shouldUseNarrowLayout ? styles.mv3 : styles.mv2, shouldUseNarrowLayout ? styles.mh5 : styles.mh2]}
+                                wrapperFocusedStyle={styles.borderColorFocus}
+                                isSearchingForReports={isSearchingForReports}
+                                selection={selection}
+                                substitutionMap={autocompleteSubstitutions}
+                                ref={textInputRef}
+                            />
+                            <SearchAutocompleteList
+                                autocompleteQueryValue={autocompleteQueryValue || textInputValue}
+                                handleSearch={searchInServer}
+                                searchQueryItem={searchQueryItem}
+                                getAdditionalSections={getAdditionalSections}
+                                onListItemPress={onListItemPress}
+                                setTextQuery={setTextAndUpdateSelection}
+                                updateAutocompleteSubstitutions={updateAutocompleteSubstitutions}
+                                onHighlightFirstItem={() => listRef.current?.updateAndScrollToFocusedIndex(1)}
+                                ref={listRef}
+                                textInputRef={textInputRef}
+                            />
+                        </>
+                    )}
+                </View>
+            </GestureDetector>
+        </GestureHandlerRootView>
     );
 }
 

From cff2bd9a1a3954d5ce64bdb2be14552ed9775c4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Musia=C5=82?= <jan.musial@swmansion.com>
Date: Tue, 8 Jul 2025 16:28:33 +0200
Subject: [PATCH 7/7] remove typo

---
 src/components/Search/SearchRouter/SearchRouter.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index fff9c46b90be..131d54416d95 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -431,7 +431,6 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
     return (
         <GestureHandlerRootView style={{flex: 1}}>
             <GestureDetector gesture={Gesture.Tap().runOnJS(true).onFinalize(Keyboard.dismiss)}>
-                {' '}
                 <View
                     style={[styles.flex1, modalWidth, styles.h100, !shouldUseNarrowLayout && styles.mh85vh]}
                     testID={SearchRouter.displayName}
