diff --git a/native/components/full-screen-view-modal.react.js b/native/components/full-screen-view-modal.react.js
--- a/native/components/full-screen-view-modal.react.js
+++ b/native/components/full-screen-view-modal.react.js
@@ -37,18 +37,11 @@
 import SWMansionIcon from './swmansion-icon.react.js';
 import ConnectedStatusBar from '../connected-status-bar.react.js';
 import type { AppNavigationProp } from '../navigation/app-navigator.react.js';
-import {
-  OverlayContext,
-  type OverlayContextType,
-} from '../navigation/overlay-context.js';
+import { OverlayContext } from '../navigation/overlay-context.js';
 import type { NavigationRoute } from '../navigation/route-names.js';
 import { useSelector } from '../redux/redux-utils.js';
-import {
-  type DerivedDimensionsInfo,
-  derivedDimensionsInfoSelector,
-} from '../selectors/dimensions-selectors.js';
+import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js';
 import type { NativeMethods } from '../types/react-native.js';
-import type { AnimatedViewStyle, ViewStyle } from '../types/styles.js';
 import type { UserProfileBottomSheetNavigationProp } from '../user-profile/user-profile-bottom-sheet-navigator.react.js';
 import { clampV2 } from '../utils/animation-utils.js';
 
@@ -71,7 +64,7 @@
   +height: number,
 };
 
-type BaseProps = {
+type Props = {
   +navigation:
     | AppNavigationProp<'ImageModal'>
     | UserProfileBottomSheetNavigationProp<'UserProfileAvatarModal'>,
@@ -83,134 +76,728 @@
   +saveContentCallback?: () => Promise<mixed>,
   +copyContentCallback?: () => mixed,
 };
-type Props = {
-  ...BaseProps,
-  // Redux state
-  +dimensions: DerivedDimensionsInfo,
-  // withOverlayContext
-  +overlayContext: ?OverlayContextType,
-  +isActive: boolean,
-  +closeButtonEnabled: boolean,
-  +actionLinksEnabled: boolean,
-  +gesture: ExclusiveGesture,
-  +closeButtonRef: { current: ?React.ElementRef<TouchableOpacityInstance> },
-  +mediaIconsRef: { current: ?React.ElementRef<typeof View> },
-  +onCloseButtonLayout: () => void,
-  +onMediaIconsLayout: () => void,
-  +close: () => void,
-  +contentViewContainerStyle: ViewStyle,
-  +animatedBackdropStyle: AnimatedViewStyle,
-  +animatedCloseButtonStyle: AnimatedViewStyle,
-  +animatedMediaIconsButtonStyle: AnimatedViewStyle,
-};
 
-class FullScreenViewModal extends React.PureComponent<Props> {
-  get contentContainerStyle(): ViewStyle {
-    const { verticalBounds } = this.props.route.params;
-    const fullScreenHeight = this.props.dimensions.height;
-    const top = verticalBounds.y;
-    const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height;
+function FullScreenViewModal(props: Props) {
+  const dimensions = useSelector(derivedDimensionsInfoSelector);
+  const overlayContext = React.useContext(OverlayContext);
 
-    // margin will clip, but padding won't
-    const verticalStyle = this.props.isActive
-      ? { paddingTop: top, paddingBottom: bottom }
-      : { marginTop: top, marginBottom: bottom };
-    return [styles.contentContainer, verticalStyle];
-  }
+  invariant(overlayContext, 'FullScreenViewModal should have OverlayContext');
 
-  render(): React.Node {
-    const { children, saveContentCallback, copyContentCallback } = this.props;
+  const isActive = !overlayContext.isDismissing;
 
-    const statusBar = this.props.isActive ? (
-      <ConnectedStatusBar hidden />
-    ) : null;
+  React.useEffect(() => {
+    if (isActive) {
+      Orientation.unlockAllOrientations();
+    } else {
+      Orientation.lockToPortrait();
+    }
+  }, [isActive]);
+
+  React.useEffect(() => {
+    return () => {
+      Orientation.lockToPortrait();
+    };
+  }, []);
+
+  const close = React.useCallback(() => {
+    props.navigation.goBackOnce();
+  }, [props.navigation]);
+
+  const [closeButtonEnabled, setCloseButtonEnabled] = React.useState(true);
+  const [actionLinksEnabled, setActionLinksEnabled] = React.useState(true);
+
+  const updateCloseButtonEnabled = React.useCallback(
+    (enabledNum: number) => {
+      const enabled = !!enabledNum;
+      if (closeButtonEnabled !== enabled) {
+        setCloseButtonEnabled(enabled);
+      }
+    },
+    [closeButtonEnabled],
+  );
+
+  const updateActionLinksEnabled = React.useCallback(
+    (enabledNum: number) => {
+      const enabled = !!enabledNum;
+      if (actionLinksEnabled !== enabled) {
+        setActionLinksEnabled(enabled);
+      }
+    },
+    [actionLinksEnabled],
+  );
+
+  const closeButtonRef =
+    React.useRef<?React.ElementRef<TouchableOpacityInstance>>();
+  const mediaIconsRef = React.useRef<?React.ElementRef<typeof View>>();
+
+  const closeButtonDimensions = useSharedValue({
+    x: -1,
+    y: -1,
+    width: 0,
+    height: 0,
+  });
 
-    let saveButton;
-    if (saveContentCallback) {
-      saveButton = (
-        <TouchableOpacity
-          onPress={saveContentCallback}
-          disabled={!this.props.actionLinksEnabled}
-          style={styles.mediaIconButtons}
-        >
-          <SWMansionIcon name="save" style={styles.mediaIcon} />
-          <Text style={styles.mediaIconText}>Save</Text>
-        </TouchableOpacity>
-      );
+  const mediaIconsDimensions = useSharedValue({
+    x: -1,
+    y: -1,
+    width: 0,
+    height: 0,
+  });
+
+  const closeButtonLastState = useSharedValue<boolean>(true);
+  const actionLinksLastState = useSharedValue<boolean>(true);
+
+  const onCloseButtonLayout = React.useCallback(() => {
+    const closeButton = closeButtonRef.current;
+    if (!closeButton) {
+      return;
     }
+    closeButton.measure((x, y, width, height, pageX, pageY) => {
+      closeButtonDimensions.value = { x: pageX, y: pageY, width, height };
+    });
+  }, [closeButtonDimensions]);
 
-    let copyButton;
-    if (Platform.OS === 'ios' && copyContentCallback) {
-      copyButton = (
-        <TouchableOpacity
-          onPress={copyContentCallback}
-          disabled={!this.props.actionLinksEnabled}
-          style={styles.mediaIconButtons}
-        >
-          <SWMansionIcon name="copy" style={styles.mediaIcon} />
-          <Text style={styles.mediaIconText}>Copy</Text>
-        </TouchableOpacity>
-      );
+  const onMediaIconsLayout = React.useCallback(() => {
+    const mediaIconsContainer = mediaIconsRef.current;
+    if (!mediaIconsContainer) {
+      return;
     }
 
-    let mediaActionButtons;
-    if (saveContentCallback || copyContentCallback) {
-      mediaActionButtons = (
-        <Animated.View
-          style={[
-            styles.mediaIconsContainer,
-            this.props.animatedMediaIconsButtonStyle,
-          ]}
-        >
-          <View
-            style={styles.mediaIconsRow}
-            onLayout={this.props.onMediaIconsLayout}
-            ref={this.props.mediaIconsRef}
-          >
-            {saveButton}
-            {copyButton}
-          </View>
-        </Animated.View>
+    mediaIconsContainer.measure((x, y, width, height, pageX, pageY) => {
+      mediaIconsDimensions.value = { x: pageX, y: pageY, width, height };
+    });
+  }, [mediaIconsDimensions]);
+
+  const outsideButtons = React.useCallback(
+    (x: number, y: number): boolean => {
+      'worklet';
+      const isOutsideButton = (dim: ButtonDimensions) => {
+        return (
+          x < dim.x ||
+          x > dim.x + dim.width ||
+          y < dim.y ||
+          y > dim.y + dim.height
+        );
+      };
+
+      const isOutsideCloseButton = isOutsideButton(closeButtonDimensions.value);
+      const isOutsideMediaIcons = isOutsideButton(mediaIconsDimensions.value);
+
+      return (
+        (closeButtonLastState.value === false || isOutsideCloseButton) &&
+        (actionLinksLastState.value === false || isOutsideMediaIcons)
       );
+    },
+    [
+      actionLinksLastState,
+      closeButtonDimensions,
+      closeButtonLastState,
+      mediaIconsDimensions,
+    ],
+  );
+
+  const curX = useSharedValue(0);
+  const curY = useSharedValue(0);
+  const curScale = useSharedValue(1);
+
+  const roundedCurScale = useDerivedValue(() => {
+    return Math.round(curScale.value * 1000) / 1000;
+  });
+
+  const centerX = useSharedValue(dimensions.width / 2);
+  const centerY = useSharedValue(dimensions.safeAreaHeight / 2);
+  const frameWidth = useSharedValue(dimensions.width);
+  const frameHeight = useSharedValue(dimensions.safeAreaHeight);
+  const imageWidth = useSharedValue(props.contentDimensions.width);
+  const imageHeight = useSharedValue(props.contentDimensions.height);
+
+  React.useEffect(() => {
+    const {
+      topInset,
+      width: newFrameWidth,
+      safeAreaHeight: newFrameHeight,
+    } = dimensions;
+    frameWidth.value = newFrameWidth;
+    frameHeight.value = newFrameHeight;
+    centerX.value = newFrameWidth / 2;
+    centerY.value = newFrameHeight / 2 + topInset;
+    const { width, height } = props.contentDimensions;
+    imageWidth.value = width;
+    imageHeight.value = height;
+  }, [
+    centerX,
+    centerY,
+    dimensions,
+    frameHeight,
+    frameWidth,
+    imageHeight,
+    imageWidth,
+    props.contentDimensions,
+  ]);
+
+  // How much space do we have to pan the image horizontally?
+  const getHorizontalPanSpace = React.useCallback(
+    (scale: number): number => {
+      'worklet';
+      const apparentWidth = imageWidth.value * scale;
+      const horizPop = (apparentWidth - frameWidth.value) / 2;
+      return Math.max(horizPop, 0);
+    },
+    [frameWidth, imageWidth],
+  );
+
+  // How much space do we have to pan the image vertically?
+  const getVerticalPanSpace = React.useCallback(
+    (scale: number): number => {
+      'worklet';
+      const apparentHeight = imageHeight.value * scale;
+      const vertPop = (apparentHeight - frameHeight.value) / 2;
+      return Math.max(vertPop, 0);
+    },
+    [frameHeight, imageHeight],
+  );
+
+  const lastPinchScale = useSharedValue(1);
+  const pinchActive = useSharedValue(false);
+
+  const pinchStart = React.useCallback(() => {
+    'worklet';
+    lastPinchScale.value = 1;
+    pinchActive.value = true;
+    cancelAnimation(curX);
+    cancelAnimation(curY);
+    cancelAnimation(curScale);
+  }, [curScale, curX, curY, lastPinchScale, pinchActive]);
+
+  const pinchUpdate = React.useCallback(
+    ({ scale, focalX, focalY }: PinchGestureEvent) => {
+      'worklet';
+      const deltaScale = scale / lastPinchScale.value;
+      const deltaPinchX =
+        (1 - deltaScale) * (focalX - curX.value - centerX.value);
+      const deltaPinchY =
+        (1 - deltaScale) * (focalY - curY.value - centerY.value);
+
+      curX.value += deltaPinchX;
+      curY.value += deltaPinchY;
+      curScale.value *= deltaScale;
+
+      lastPinchScale.value = scale;
+    },
+    [centerX, centerY, curScale, curX, curY, lastPinchScale],
+  );
+
+  const pinchEnd = React.useCallback(() => {
+    'worklet';
+    pinchActive.value = false;
+  }, [pinchActive]);
+
+  const panActive = useSharedValue(false);
+
+  const lastPanTranslationX = useSharedValue(0);
+  const lastPanTranslationY = useSharedValue(0);
+
+  const panStart = React.useCallback(
+    ({ absoluteX, absoluteY, translationX, translationY }: PanGestureEvent) => {
+      'worklet';
+      lastPanTranslationX.value = 0;
+      lastPanTranslationY.value = 0;
+      panActive.value = outsideButtons(
+        absoluteX - translationX,
+        absoluteY - translationY,
+      );
+      if (panActive.value) {
+        cancelAnimation(curX);
+        cancelAnimation(curY);
+        cancelAnimation(curScale);
+      }
+    },
+    [
+      lastPanTranslationX,
+      lastPanTranslationY,
+      outsideButtons,
+      panActive,
+      curX,
+      curY,
+      curScale,
+    ],
+  );
+
+  const panUpdate = React.useCallback(
+    ({ translationX, translationY }: PanGestureEvent) => {
+      'worklet';
+      if (!panActive.value) {
+        return;
+      }
+      curX.value += translationX - lastPanTranslationX.value;
+      curY.value += translationY - lastPanTranslationY.value;
+      lastPanTranslationX.value = translationX;
+      lastPanTranslationY.value = translationY;
+    },
+    [curX, curY, lastPanTranslationX, lastPanTranslationY, panActive],
+  );
+
+  const progressiveOpacity = useDerivedValue(() => {
+    return Math.max(
+      Math.min(
+        1 - Math.abs(curX.value / frameWidth.value),
+        1 - Math.abs(curY.value / frameHeight.value),
+      ),
+      0,
+    );
+  });
+
+  const isRunningDismissAnimation = useSharedValue(false);
+
+  const panEnd = React.useCallback(
+    ({ velocityX, velocityY }: PanGestureEvent) => {
+      'worklet';
+      if (!panActive.value) {
+        return;
+      }
+      panActive.value = false;
+      const velocity = Math.pow(
+        Math.pow(velocityX, 2) + Math.pow(velocityY, 2),
+        0.5,
+      );
+      const shouldGoBack = velocity > 50 || 0.7 > progressiveOpacity.value;
+      if (shouldGoBack && !pinchActive.value && roundedCurScale.value <= 1) {
+        isRunningDismissAnimation.value = true;
+        curX.value = withDecay({ velocity: velocityX, ...decayConfig });
+        curY.value = withDecay({ velocity: velocityY, ...decayConfig });
+        cancelAnimation(curScale);
+        runOnJS(close)();
+      } else {
+        const recenteredScale = Math.max(curScale.value, 1);
+        const horizontalPanSpace = getHorizontalPanSpace(recenteredScale);
+        const verticalPanSpace = getVerticalPanSpace(recenteredScale);
+        curX.value = withDecay({
+          velocity: velocityX,
+          clamp: [-horizontalPanSpace, horizontalPanSpace],
+          ...decayConfig,
+        });
+        curY.value = withDecay({
+          velocity: velocityY,
+          clamp: [-verticalPanSpace, verticalPanSpace],
+          ...decayConfig,
+        });
+      }
+    },
+    [
+      panActive,
+      progressiveOpacity,
+      pinchActive,
+      roundedCurScale,
+      isRunningDismissAnimation,
+      curX,
+      curY,
+      close,
+      curScale,
+      getHorizontalPanSpace,
+      getVerticalPanSpace,
+    ],
+  );
+
+  const curCloseButtonOpacity = useSharedValue(1);
+  const curActionLinksOpacity = useSharedValue(1);
+  const targetCloseButtonOpacity = useSharedValue<0 | 1>(1);
+  const targetActionLinksOpacity = useSharedValue<0 | 1>(1);
+
+  const toggleCloseButton = React.useCallback(() => {
+    'worklet';
+    targetCloseButtonOpacity.value =
+      targetCloseButtonOpacity.value === 0 ? 1 : 0;
+    curCloseButtonOpacity.value = withTiming(
+      targetCloseButtonOpacity.value,
+      defaultTimingConfig,
+      isFinished => {
+        if (isFinished) {
+          runOnJS(updateCloseButtonEnabled)(targetCloseButtonOpacity.value);
+        }
+      },
+    );
+  }, [
+    curCloseButtonOpacity,
+    targetCloseButtonOpacity,
+    updateCloseButtonEnabled,
+  ]);
+
+  const toggleActionLinks = React.useCallback(() => {
+    'worklet';
+    targetActionLinksOpacity.value =
+      targetActionLinksOpacity.value === 0 ? 1 : 0;
+    curActionLinksOpacity.value = withTiming(
+      targetActionLinksOpacity.value,
+      defaultTimingConfig,
+      isFinished => {
+        if (isFinished) {
+          runOnJS(updateActionLinksEnabled)(targetActionLinksOpacity.value);
+        }
+      },
+    );
+  }, [
+    curActionLinksOpacity,
+    targetActionLinksOpacity,
+    updateActionLinksEnabled,
+  ]);
+
+  useAnimatedReaction(
+    () => roundedCurScale.value > 1,
+    (isZoomed, wasZoomed) => {
+      // when image is zoomed in then assure action target links are hidden
+      if (isZoomed && targetActionLinksOpacity.value === 1) {
+        toggleActionLinks();
+      }
+      // when image becomes unzoomed then toggle buttons opacity accordingly
+      if (wasZoomed && !isZoomed) {
+        if (targetCloseButtonOpacity.value === 0) {
+          toggleCloseButton();
+        }
+        toggleActionLinks();
+      }
+    },
+  );
+
+  const singleTapUpdate = React.useCallback(
+    ({ x, y }: TapGestureEvent) => {
+      'worklet';
+      if (!outsideButtons(x, y)) {
+        return;
+      }
+      toggleCloseButton();
+      if (roundedCurScale.value <= 1) {
+        toggleActionLinks();
+      }
+    },
+    [outsideButtons, toggleActionLinks, toggleCloseButton, roundedCurScale],
+  );
+
+  const isRunningDoubleTapZoomAnimation = useSharedValue(false);
+
+  const doubleTapUpdate = React.useCallback(
+    ({ x, y }: TapGestureEvent) => {
+      'worklet';
+      if (!outsideButtons(x, y)) {
+        return;
+      }
+      const targetScale = roundedCurScale.value > 1 ? 1 : 3;
+
+      const tapXDiff = x - centerX.value - curX.value;
+      const tapYDiff = y - centerY.value - curY.value;
+      const tapXPercent = tapXDiff / imageWidth.value / curScale.value;
+      const tapYPercent = tapYDiff / imageHeight.value / curScale.value;
+
+      const horizPanSpace = getHorizontalPanSpace(targetScale);
+      const vertPanSpace = getVerticalPanSpace(targetScale);
+      const horizPanPercent = horizPanSpace / imageWidth.value / targetScale;
+      const vertPanPercent = vertPanSpace / imageHeight.value / targetScale;
+
+      const tapXPercentClamped = clampV2(
+        tapXPercent,
+        -horizPanPercent,
+        horizPanPercent,
+      );
+      const tapYPercentClamped = clampV2(
+        tapYPercent,
+        -vertPanPercent,
+        vertPanPercent,
+      );
+
+      const targetX = tapXPercentClamped * imageWidth.value * targetScale;
+      const targetY = tapYPercentClamped * imageHeight.value * targetScale;
+
+      isRunningDoubleTapZoomAnimation.value = true;
+      curScale.value = withTiming(
+        targetScale,
+        defaultTimingConfig,
+        () => (isRunningDoubleTapZoomAnimation.value = false),
+      );
+      curX.value = withTiming(targetX, defaultTimingConfig);
+      curY.value = withTiming(targetY, defaultTimingConfig);
+    },
+    [
+      centerX,
+      centerY,
+      curScale,
+      curX,
+      curY,
+      getHorizontalPanSpace,
+      imageHeight,
+      imageWidth,
+      outsideButtons,
+      roundedCurScale,
+      getVerticalPanSpace,
+      isRunningDoubleTapZoomAnimation,
+    ],
+  );
+
+  const backdropReset = useSharedValue(1);
+
+  useAnimatedReaction(
+    () => pinchActive.value || roundedCurScale.value > 1,
+    (isReset, wasReset) => {
+      if (isReset && !wasReset) {
+        backdropReset.value = progressiveOpacity.value;
+        backdropReset.value = withTiming(1, defaultTimingConfig);
+      }
+    },
+  );
+
+  const curBackdropOpacity = useDerivedValue(() => {
+    if (pinchActive.value || roundedCurScale.value > 1) {
+      return backdropReset.value;
     }
+    return progressiveOpacity.value;
+  });
+
+  useAnimatedReaction(
+    () =>
+      pinchActive.value ||
+      panActive.value ||
+      isRunningDismissAnimation.value ||
+      isRunningDoubleTapZoomAnimation.value,
+    activeInteraction => {
+      if (activeInteraction) {
+        return;
+      }
+      const recenteredScale = Math.max(curScale.value, 1);
+      const horizontalPanSpace = getHorizontalPanSpace(recenteredScale);
+      const verticalPanSpace = getVerticalPanSpace(recenteredScale);
+      const recenteredX = clampV2(
+        curX.value,
+        -horizontalPanSpace,
+        horizontalPanSpace,
+      );
+      const recenteredY = clampV2(
+        curY.value,
+        -verticalPanSpace,
+        verticalPanSpace,
+      );
+      if (curScale.value !== recenteredScale) {
+        curScale.value = withTiming(recenteredScale, defaultTimingConfig);
+      }
+      if (curX.value !== recenteredX) {
+        curX.value = withTiming(recenteredX, defaultTimingConfig);
+      }
+      if (curY.value !== recenteredY) {
+        curY.value = withTiming(recenteredY, defaultTimingConfig);
+      }
+    },
+  );
+
+  const gesture = React.useMemo(() => {
+    const pinchGesture = Gesture.Pinch()
+      .onStart(pinchStart)
+      .onUpdate(pinchUpdate)
+      .onEnd(pinchEnd);
+    const panGesture = Gesture.Pan()
+      .averageTouches(true)
+      .onStart(panStart)
+      .onUpdate(panUpdate)
+      .onEnd(panEnd);
+    const doubleTapGesture = Gesture.Tap()
+      .numberOfTaps(2)
+      .onEnd(doubleTapUpdate);
+    const singleTapGesture = Gesture.Tap()
+      .numberOfTaps(1)
+      .onEnd(singleTapUpdate);
+
+    return Gesture.Exclusive(
+      Gesture.Simultaneous(pinchGesture, panGesture),
+      doubleTapGesture,
+      singleTapGesture,
+    );
+  }, [
+    doubleTapUpdate,
+    panEnd,
+    panStart,
+    panUpdate,
+    pinchStart,
+    pinchEnd,
+    pinchUpdate,
+    singleTapUpdate,
+  ]);
+
+  const navigationProgress = overlayContext.positionV2;
+  invariant(
+    navigationProgress,
+    'position should be defined in FullScreenViewModal',
+  );
+
+  const { contentDimensions } = props;
+  const { verticalBounds, initialCoordinates } = props.route.params;
+
+  const contentViewContainerStyle = useAnimatedStyle(() => {
+    const { height, width } = contentDimensions;
+    const {
+      safeAreaHeight: dimFrameHeight,
+      width: dimFrameWidth,
+      topInset,
+    } = dimensions;
+
+    const left = centerX.value - imageWidth.value / 2;
+    const top = centerY.value - imageHeight.value / 2;
+
+    const initialScale = initialCoordinates.width / imageWidth.value;
+    const initialTranslateX =
+      initialCoordinates.x +
+      initialCoordinates.width / 2 -
+      (left + imageWidth.value / 2);
+    const initialTranslateY =
+      initialCoordinates.y +
+      initialCoordinates.height / 2 -
+      (top + imageHeight.value / 2);
+
+    const reverseNavigationProgress = 1 - navigationProgress.value;
+    const scale =
+      reverseNavigationProgress * initialScale +
+      navigationProgress.value * curScale.value;
+    const x =
+      reverseNavigationProgress * initialTranslateX +
+      navigationProgress.value * curX.value;
+    const y =
+      reverseNavigationProgress * initialTranslateY +
+      navigationProgress.value * curY.value;
+
+    const imageContainerOpacity = interpolate(
+      navigationProgress.value,
+      [0, 0.1],
+      [0, 1],
+      Extrapolate.CLAMP,
+    );
+
+    return {
+      height,
+      width,
+      marginTop: (dimFrameHeight - height) / 2 + topInset - verticalBounds.y,
+      marginLeft: (dimFrameWidth - width) / 2,
+      opacity: imageContainerOpacity,
+      transform: [{ translateX: x }, { translateY: y }, { scale }],
+    };
+  }, [contentDimensions, verticalBounds, initialCoordinates, dimensions]);
+
+  const animatedBackdropStyle = useAnimatedStyle(() => ({
+    opacity: navigationProgress.value * curBackdropOpacity.value,
+  }));
+
+  const buttonOpacity = useDerivedValue(() =>
+    interpolate(curBackdropOpacity.value, [0.95, 1], [0, 1], Extrapolate.CLAMP),
+  );
+
+  const animatedCloseButtonStyle = useAnimatedStyle(() => {
+    const closeButtonOpacity =
+      navigationProgress.value *
+      buttonOpacity.value *
+      curCloseButtonOpacity.value;
+    return {
+      opacity: closeButtonOpacity,
+    };
+  });
+
+  const animatedMediaIconsButtonStyle = useAnimatedStyle(() => {
+    const actionLinksOpacity =
+      navigationProgress.value *
+      buttonOpacity.value *
+      curActionLinksOpacity.value;
+    return {
+      opacity: actionLinksOpacity,
+    };
+  });
+
+  const contentContainerStyle = React.useMemo(() => {
+    const fullScreenHeight = dimensions.height;
+    const top = verticalBounds.y;
+    const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height;
+
+    // margin will clip, but padding won't
+    const verticalStyle = isActive
+      ? { paddingTop: top, paddingBottom: bottom }
+      : { marginTop: top, marginBottom: bottom };
+    return [styles.contentContainer, verticalStyle];
+  }, [dimensions.height, isActive, verticalBounds.height, verticalBounds.y]);
+
+  const { children, saveContentCallback, copyContentCallback } = props;
+
+  const statusBar = isActive ? <ConnectedStatusBar hidden /> : null;
+
+  let saveButton;
+  if (saveContentCallback) {
+    saveButton = (
+      <TouchableOpacity
+        onPress={saveContentCallback}
+        disabled={!actionLinksEnabled}
+        style={styles.mediaIconButtons}
+      >
+        <SWMansionIcon name="save" style={styles.mediaIcon} />
+        <Text style={styles.mediaIconText}>Save</Text>
+      </TouchableOpacity>
+    );
+  }
+
+  let copyButton;
+  if (Platform.OS === 'ios' && copyContentCallback) {
+    copyButton = (
+      <TouchableOpacity
+        onPress={copyContentCallback}
+        disabled={!actionLinksEnabled}
+        style={styles.mediaIconButtons}
+      >
+        <SWMansionIcon name="copy" style={styles.mediaIcon} />
+        <Text style={styles.mediaIconText}>Copy</Text>
+      </TouchableOpacity>
+    );
+  }
 
-    const view = (
-      <Animated.View style={styles.container}>
-        {statusBar}
-        <Animated.View
-          style={[styles.backdrop, this.props.animatedBackdropStyle]}
-        />
-        <View style={this.contentContainerStyle}>
-          <Animated.View style={this.props.contentViewContainerStyle}>
-            {children}
-          </Animated.View>
+  let mediaActionButtons;
+  if (saveContentCallback || copyContentCallback) {
+    mediaActionButtons = (
+      <Animated.View
+        style={[styles.mediaIconsContainer, animatedMediaIconsButtonStyle]}
+      >
+        <View
+          style={styles.mediaIconsRow}
+          onLayout={onMediaIconsLayout}
+          ref={mediaIconsRef}
+        >
+          {saveButton}
+          {copyButton}
         </View>
-        <SafeAreaView style={styles.buttonsOverlay}>
-          <View style={styles.fill}>
-            <Animated.View
-              style={[
-                styles.closeButtonContainer,
-                this.props.animatedCloseButtonStyle,
-              ]}
-            >
-              <TouchableOpacity
-                onPress={this.props.close}
-                disabled={!this.props.closeButtonEnabled}
-                onLayout={this.props.onCloseButtonLayout}
-                ref={this.props.closeButtonRef}
-              >
-                <Text style={styles.closeButton}>×</Text>
-              </TouchableOpacity>
-            </Animated.View>
-            {mediaActionButtons}
-          </View>
-        </SafeAreaView>
       </Animated.View>
     );
-    return (
-      <GestureDetector gesture={this.props.gesture}>{view}</GestureDetector>
-    );
   }
+
+  return (
+    <Animated.View style={styles.container}>
+      <GestureDetector gesture={gesture}>
+        <Animated.View style={styles.container}>
+          {statusBar}
+          <Animated.View style={[styles.backdrop, animatedBackdropStyle]} />
+          <View style={contentContainerStyle}>
+            <Animated.View style={contentViewContainerStyle}>
+              {children}
+            </Animated.View>
+          </View>
+          <SafeAreaView style={styles.buttonsOverlay}>
+            <View style={styles.fill}>
+              <Animated.View
+                style={[styles.closeButtonContainer, animatedCloseButtonStyle]}
+              >
+                <TouchableOpacity
+                  onPress={close}
+                  disabled={!closeButtonEnabled}
+                  onLayout={onCloseButtonLayout}
+                  ref={closeButtonRef}
+                >
+                  <Text style={styles.closeButton}>×</Text>
+                </TouchableOpacity>
+              </Animated.View>
+              {mediaActionButtons}
+            </View>
+          </SafeAreaView>
+        </Animated.View>
+      </GestureDetector>
+    </Animated.View>
+  );
 }
 
 const styles = StyleSheet.create({
@@ -284,668 +871,7 @@
   },
 });
 
-const ConnectedFullScreenViewModal: React.ComponentType<BaseProps> =
-  React.memo<BaseProps>(function ConnectedFullScreenViewModal(
-    props: BaseProps,
-  ) {
-    const dimensions = useSelector(derivedDimensionsInfoSelector);
-    const overlayContext = React.useContext(OverlayContext);
-
-    invariant(overlayContext, 'FullScreenViewModal should have OverlayContext');
-
-    const isActive = !overlayContext.isDismissing;
-
-    React.useEffect(() => {
-      if (isActive) {
-        Orientation.unlockAllOrientations();
-      } else {
-        Orientation.lockToPortrait();
-      }
-    }, [isActive]);
-
-    React.useEffect(() => {
-      return () => {
-        Orientation.lockToPortrait();
-      };
-    }, []);
-
-    const close = React.useCallback(() => {
-      props.navigation.goBackOnce();
-    }, [props.navigation]);
-
-    const [closeButtonEnabled, setCloseButtonEnabled] = React.useState(true);
-    const [actionLinksEnabled, setActionLinksEnabled] = React.useState(true);
-
-    const updateCloseButtonEnabled = React.useCallback(
-      (enabledNum: number) => {
-        const enabled = !!enabledNum;
-        if (closeButtonEnabled !== enabled) {
-          setCloseButtonEnabled(enabled);
-        }
-      },
-      [closeButtonEnabled],
-    );
-
-    const updateActionLinksEnabled = React.useCallback(
-      (enabledNum: number) => {
-        const enabled = !!enabledNum;
-        if (actionLinksEnabled !== enabled) {
-          setActionLinksEnabled(enabled);
-        }
-      },
-      [actionLinksEnabled],
-    );
-
-    const closeButtonRef =
-      React.useRef<?React.ElementRef<TouchableOpacityInstance>>();
-    const mediaIconsRef = React.useRef<?React.ElementRef<typeof View>>();
-
-    const closeButtonDimensions = useSharedValue({
-      x: -1,
-      y: -1,
-      width: 0,
-      height: 0,
-    });
-
-    const mediaIconsDimensions = useSharedValue({
-      x: -1,
-      y: -1,
-      width: 0,
-      height: 0,
-    });
-
-    const closeButtonLastState = useSharedValue<boolean>(true);
-    const actionLinksLastState = useSharedValue<boolean>(true);
-
-    const onCloseButtonLayout = React.useCallback(() => {
-      const closeButton = closeButtonRef.current;
-      if (!closeButton) {
-        return;
-      }
-      closeButton.measure((x, y, width, height, pageX, pageY) => {
-        closeButtonDimensions.value = { x: pageX, y: pageY, width, height };
-      });
-    }, [closeButtonDimensions]);
-
-    const onMediaIconsLayout = React.useCallback(() => {
-      const mediaIconsContainer = mediaIconsRef.current;
-      if (!mediaIconsContainer) {
-        return;
-      }
-
-      mediaIconsContainer.measure((x, y, width, height, pageX, pageY) => {
-        mediaIconsDimensions.value = { x: pageX, y: pageY, width, height };
-      });
-    }, [mediaIconsDimensions]);
-
-    const outsideButtons = React.useCallback(
-      (x: number, y: number): boolean => {
-        'worklet';
-        const isOutsideButton = (dim: ButtonDimensions) => {
-          return (
-            x < dim.x ||
-            x > dim.x + dim.width ||
-            y < dim.y ||
-            y > dim.y + dim.height
-          );
-        };
-
-        const isOutsideCloseButton = isOutsideButton(
-          closeButtonDimensions.value,
-        );
-        const isOutsideMediaIcons = isOutsideButton(mediaIconsDimensions.value);
-
-        return (
-          (closeButtonLastState.value === false || isOutsideCloseButton) &&
-          (actionLinksLastState.value === false || isOutsideMediaIcons)
-        );
-      },
-      [
-        actionLinksLastState,
-        closeButtonDimensions,
-        closeButtonLastState,
-        mediaIconsDimensions,
-      ],
-    );
-
-    const curX = useSharedValue(0);
-    const curY = useSharedValue(0);
-    const curScale = useSharedValue(1);
-
-    const roundedCurScale = useDerivedValue(() => {
-      return Math.round(curScale.value * 1000) / 1000;
-    });
-
-    const centerX = useSharedValue(dimensions.width / 2);
-    const centerY = useSharedValue(dimensions.safeAreaHeight / 2);
-    const frameWidth = useSharedValue(dimensions.width);
-    const frameHeight = useSharedValue(dimensions.safeAreaHeight);
-    const imageWidth = useSharedValue(props.contentDimensions.width);
-    const imageHeight = useSharedValue(props.contentDimensions.height);
-
-    React.useEffect(() => {
-      const {
-        topInset,
-        width: newFrameWidth,
-        safeAreaHeight: newFrameHeight,
-      } = dimensions;
-      frameWidth.value = newFrameWidth;
-      frameHeight.value = newFrameHeight;
-      centerX.value = newFrameWidth / 2;
-      centerY.value = newFrameHeight / 2 + topInset;
-      const { width, height } = props.contentDimensions;
-      imageWidth.value = width;
-      imageHeight.value = height;
-    }, [
-      centerX,
-      centerY,
-      dimensions,
-      frameHeight,
-      frameWidth,
-      imageHeight,
-      imageWidth,
-      props.contentDimensions,
-    ]);
-
-    // How much space do we have to pan the image horizontally?
-    const getHorizontalPanSpace = React.useCallback(
-      (scale: number): number => {
-        'worklet';
-        const apparentWidth = imageWidth.value * scale;
-        const horizPop = (apparentWidth - frameWidth.value) / 2;
-        return Math.max(horizPop, 0);
-      },
-      [frameWidth, imageWidth],
-    );
-
-    // How much space do we have to pan the image vertically?
-    const getVerticalPanSpace = React.useCallback(
-      (scale: number): number => {
-        'worklet';
-        const apparentHeight = imageHeight.value * scale;
-        const vertPop = (apparentHeight - frameHeight.value) / 2;
-        return Math.max(vertPop, 0);
-      },
-      [frameHeight, imageHeight],
-    );
-
-    const lastPinchScale = useSharedValue(1);
-    const pinchActive = useSharedValue(false);
-
-    const pinchStart = React.useCallback(() => {
-      'worklet';
-      lastPinchScale.value = 1;
-      pinchActive.value = true;
-      cancelAnimation(curX);
-      cancelAnimation(curY);
-      cancelAnimation(curScale);
-    }, [curScale, curX, curY, lastPinchScale, pinchActive]);
-
-    const pinchUpdate = React.useCallback(
-      ({ scale, focalX, focalY }: PinchGestureEvent) => {
-        'worklet';
-        const deltaScale = scale / lastPinchScale.value;
-        const deltaPinchX =
-          (1 - deltaScale) * (focalX - curX.value - centerX.value);
-        const deltaPinchY =
-          (1 - deltaScale) * (focalY - curY.value - centerY.value);
-
-        curX.value += deltaPinchX;
-        curY.value += deltaPinchY;
-        curScale.value *= deltaScale;
-
-        lastPinchScale.value = scale;
-      },
-      [centerX, centerY, curScale, curX, curY, lastPinchScale],
-    );
-
-    const pinchEnd = React.useCallback(() => {
-      'worklet';
-      pinchActive.value = false;
-    }, [pinchActive]);
-
-    const panActive = useSharedValue(false);
-
-    const lastPanTranslationX = useSharedValue(0);
-    const lastPanTranslationY = useSharedValue(0);
-
-    const panStart = React.useCallback(
-      ({
-        absoluteX,
-        absoluteY,
-        translationX,
-        translationY,
-      }: PanGestureEvent) => {
-        'worklet';
-        lastPanTranslationX.value = 0;
-        lastPanTranslationY.value = 0;
-        panActive.value = outsideButtons(
-          absoluteX - translationX,
-          absoluteY - translationY,
-        );
-        if (panActive.value) {
-          cancelAnimation(curX);
-          cancelAnimation(curY);
-          cancelAnimation(curScale);
-        }
-      },
-      [
-        lastPanTranslationX,
-        lastPanTranslationY,
-        outsideButtons,
-        panActive,
-        curX,
-        curY,
-        curScale,
-      ],
-    );
-
-    const panUpdate = React.useCallback(
-      ({ translationX, translationY }: PanGestureEvent) => {
-        'worklet';
-        if (!panActive.value) {
-          return;
-        }
-        curX.value += translationX - lastPanTranslationX.value;
-        curY.value += translationY - lastPanTranslationY.value;
-        lastPanTranslationX.value = translationX;
-        lastPanTranslationY.value = translationY;
-      },
-      [curX, curY, lastPanTranslationX, lastPanTranslationY, panActive],
-    );
-
-    const progressiveOpacity = useDerivedValue(() => {
-      return Math.max(
-        Math.min(
-          1 - Math.abs(curX.value / frameWidth.value),
-          1 - Math.abs(curY.value / frameHeight.value),
-        ),
-        0,
-      );
-    });
-
-    const isRunningDismissAnimation = useSharedValue(false);
-
-    const panEnd = React.useCallback(
-      ({ velocityX, velocityY }: PanGestureEvent) => {
-        'worklet';
-        if (!panActive.value) {
-          return;
-        }
-        panActive.value = false;
-        const velocity = Math.pow(
-          Math.pow(velocityX, 2) + Math.pow(velocityY, 2),
-          0.5,
-        );
-        const shouldGoBack = velocity > 50 || 0.7 > progressiveOpacity.value;
-        if (shouldGoBack && !pinchActive.value && roundedCurScale.value <= 1) {
-          isRunningDismissAnimation.value = true;
-          curX.value = withDecay({ velocity: velocityX, ...decayConfig });
-          curY.value = withDecay({ velocity: velocityY, ...decayConfig });
-          cancelAnimation(curScale);
-          runOnJS(close)();
-        } else {
-          const recenteredScale = Math.max(curScale.value, 1);
-          const horizontalPanSpace = getHorizontalPanSpace(recenteredScale);
-          const verticalPanSpace = getVerticalPanSpace(recenteredScale);
-          curX.value = withDecay({
-            velocity: velocityX,
-            clamp: [-horizontalPanSpace, horizontalPanSpace],
-            ...decayConfig,
-          });
-          curY.value = withDecay({
-            velocity: velocityY,
-            clamp: [-verticalPanSpace, verticalPanSpace],
-            ...decayConfig,
-          });
-        }
-      },
-      [
-        panActive,
-        progressiveOpacity,
-        pinchActive,
-        roundedCurScale,
-        curScale,
-        isRunningDismissAnimation,
-        curX,
-        curY,
-        close,
-        getHorizontalPanSpace,
-        getVerticalPanSpace,
-      ],
-    );
-
-    const curCloseButtonOpacity = useSharedValue(1);
-    const curActionLinksOpacity = useSharedValue(1);
-    const targetCloseButtonOpacity = useSharedValue<0 | 1>(1);
-    const targetActionLinksOpacity = useSharedValue<0 | 1>(1);
-
-    const toggleCloseButton = React.useCallback(() => {
-      'worklet';
-      targetCloseButtonOpacity.value =
-        targetCloseButtonOpacity.value === 0 ? 1 : 0;
-      curCloseButtonOpacity.value = withTiming(
-        targetCloseButtonOpacity.value,
-        defaultTimingConfig,
-        isFinished => {
-          if (isFinished) {
-            runOnJS(updateCloseButtonEnabled)(targetCloseButtonOpacity.value);
-          }
-        },
-      );
-    }, [
-      curCloseButtonOpacity,
-      targetCloseButtonOpacity,
-      updateCloseButtonEnabled,
-    ]);
-
-    const toggleActionLinks = React.useCallback(() => {
-      'worklet';
-      targetActionLinksOpacity.value =
-        targetActionLinksOpacity.value === 0 ? 1 : 0;
-      curActionLinksOpacity.value = withTiming(
-        targetActionLinksOpacity.value,
-        defaultTimingConfig,
-        isFinished => {
-          if (isFinished) {
-            runOnJS(updateActionLinksEnabled)(targetActionLinksOpacity.value);
-          }
-        },
-      );
-    }, [
-      curActionLinksOpacity,
-      targetActionLinksOpacity,
-      updateActionLinksEnabled,
-    ]);
-
-    useAnimatedReaction(
-      () => roundedCurScale.value > 1,
-      (isZoomed, wasZoomed) => {
-        // when image is zoomed in then assure action target links are hidden
-        if (isZoomed && targetActionLinksOpacity.value === 1) {
-          toggleActionLinks();
-        }
-        // when image becomes unzoomed then toggle buttons opacity accordingly
-        if (wasZoomed && !isZoomed) {
-          if (targetCloseButtonOpacity.value === 0) {
-            toggleCloseButton();
-          }
-          toggleActionLinks();
-        }
-      },
-    );
-
-    const singleTapUpdate = React.useCallback(
-      ({ x, y }: TapGestureEvent) => {
-        'worklet';
-        if (!outsideButtons(x, y)) {
-          return;
-        }
-        toggleCloseButton();
-        if (roundedCurScale.value <= 1) {
-          toggleActionLinks();
-        }
-      },
-      [outsideButtons, toggleActionLinks, toggleCloseButton, roundedCurScale],
-    );
-
-    const isRunningDoubleTapZoomAnimation = useSharedValue(false);
-
-    const doubleTapUpdate = React.useCallback(
-      ({ x, y }: TapGestureEvent) => {
-        'worklet';
-        if (!outsideButtons(x, y)) {
-          return;
-        }
-        const targetScale = roundedCurScale.value > 1 ? 1 : 3;
-
-        const tapXDiff = x - centerX.value - curX.value;
-        const tapYDiff = y - centerY.value - curY.value;
-        const tapXPercent = tapXDiff / imageWidth.value / curScale.value;
-        const tapYPercent = tapYDiff / imageHeight.value / curScale.value;
-
-        const horizPanSpace = getHorizontalPanSpace(targetScale);
-        const vertPanSpace = getVerticalPanSpace(targetScale);
-        const horizPanPercent = horizPanSpace / imageWidth.value / targetScale;
-        const vertPanPercent = vertPanSpace / imageHeight.value / targetScale;
-
-        const tapXPercentClamped = clampV2(
-          tapXPercent,
-          -horizPanPercent,
-          horizPanPercent,
-        );
-        const tapYPercentClamped = clampV2(
-          tapYPercent,
-          -vertPanPercent,
-          vertPanPercent,
-        );
-
-        const targetX = tapXPercentClamped * imageWidth.value * targetScale;
-        const targetY = tapYPercentClamped * imageHeight.value * targetScale;
-
-        isRunningDoubleTapZoomAnimation.value = true;
-        curScale.value = withTiming(
-          targetScale,
-          defaultTimingConfig,
-          () => (isRunningDoubleTapZoomAnimation.value = false),
-        );
-        curX.value = withTiming(targetX, defaultTimingConfig);
-        curY.value = withTiming(targetY, defaultTimingConfig);
-      },
-      [
-        centerX,
-        centerY,
-        curScale,
-        curX,
-        curY,
-        getHorizontalPanSpace,
-        imageHeight,
-        imageWidth,
-        outsideButtons,
-        roundedCurScale,
-        getVerticalPanSpace,
-        isRunningDoubleTapZoomAnimation,
-      ],
-    );
-
-    const backdropReset = useSharedValue(1);
-
-    useAnimatedReaction(
-      () => pinchActive.value || roundedCurScale.value > 1,
-      (isReset, wasReset) => {
-        if (isReset && !wasReset) {
-          backdropReset.value = progressiveOpacity.value;
-          backdropReset.value = withTiming(1, defaultTimingConfig);
-        }
-      },
-    );
-
-    const curBackdropOpacity = useDerivedValue(() => {
-      if (pinchActive.value || roundedCurScale.value > 1) {
-        return backdropReset.value;
-      }
-      return progressiveOpacity.value;
-    });
-
-    useAnimatedReaction(
-      () =>
-        pinchActive.value ||
-        panActive.value ||
-        isRunningDismissAnimation.value ||
-        isRunningDoubleTapZoomAnimation.value,
-      activeInteraction => {
-        if (activeInteraction) {
-          return;
-        }
-        const recenteredScale = Math.max(curScale.value, 1);
-        const horizontalPanSpace = getHorizontalPanSpace(recenteredScale);
-        const verticalPanSpace = getVerticalPanSpace(recenteredScale);
-        const recenteredX = clampV2(
-          curX.value,
-          -horizontalPanSpace,
-          horizontalPanSpace,
-        );
-        const recenteredY = clampV2(
-          curY.value,
-          -verticalPanSpace,
-          verticalPanSpace,
-        );
-        if (curScale.value !== recenteredScale) {
-          curScale.value = withTiming(recenteredScale, defaultTimingConfig);
-        }
-        if (curX.value !== recenteredX) {
-          curX.value = withTiming(recenteredX, defaultTimingConfig);
-        }
-        if (curY.value !== recenteredY) {
-          curY.value = withTiming(recenteredY, defaultTimingConfig);
-        }
-      },
-    );
-
-    const gesture = React.useMemo(() => {
-      const pinchGesture = Gesture.Pinch()
-        .onStart(pinchStart)
-        .onUpdate(pinchUpdate)
-        .onEnd(pinchEnd);
-      const panGesture = Gesture.Pan()
-        .averageTouches(true)
-        .onStart(panStart)
-        .onUpdate(panUpdate)
-        .onEnd(panEnd);
-      const doubleTapGesture = Gesture.Tap()
-        .numberOfTaps(2)
-        .onEnd(doubleTapUpdate);
-      const singleTapGesture = Gesture.Tap()
-        .numberOfTaps(1)
-        .onEnd(singleTapUpdate);
-
-      return Gesture.Exclusive(
-        Gesture.Simultaneous(pinchGesture, panGesture),
-        doubleTapGesture,
-        singleTapGesture,
-      );
-    }, [
-      doubleTapUpdate,
-      panEnd,
-      panStart,
-      panUpdate,
-      pinchStart,
-      pinchEnd,
-      pinchUpdate,
-      singleTapUpdate,
-    ]);
-
-    const navigationProgress = overlayContext.positionV2;
-    invariant(
-      navigationProgress,
-      'position should be defined in FullScreenViewModal',
-    );
-
-    const { contentDimensions } = props;
-    const { verticalBounds, initialCoordinates } = props.route.params;
-
-    const contentViewContainerStyle = useAnimatedStyle(() => {
-      const { height, width } = contentDimensions;
-      const {
-        safeAreaHeight: dimFrameHeight,
-        width: dimFrameWidth,
-        topInset,
-      } = dimensions;
-
-      const left = centerX.value - imageWidth.value / 2;
-      const top = centerY.value - imageHeight.value / 2;
-
-      const initialScale = initialCoordinates.width / imageWidth.value;
-      const initialTranslateX =
-        initialCoordinates.x +
-        initialCoordinates.width / 2 -
-        (left + imageWidth.value / 2);
-      const initialTranslateY =
-        initialCoordinates.y +
-        initialCoordinates.height / 2 -
-        (top + imageHeight.value / 2);
-
-      const reverseNavigationProgress = 1 - navigationProgress.value;
-      const scale =
-        reverseNavigationProgress * initialScale +
-        navigationProgress.value * curScale.value;
-      const x =
-        reverseNavigationProgress * initialTranslateX +
-        navigationProgress.value * curX.value;
-      const y =
-        reverseNavigationProgress * initialTranslateY +
-        navigationProgress.value * curY.value;
-
-      const imageContainerOpacity = interpolate(
-        navigationProgress.value,
-        [0, 0.1],
-        [0, 1],
-        Extrapolate.CLAMP,
-      );
-
-      return {
-        height,
-        width,
-        marginTop: (dimFrameHeight - height) / 2 + topInset - verticalBounds.y,
-        marginLeft: (dimFrameWidth - width) / 2,
-        opacity: imageContainerOpacity,
-        transform: [{ translateX: x }, { translateY: y }, { scale }],
-      };
-    }, [contentDimensions, verticalBounds, initialCoordinates, dimensions]);
-
-    const animatedBackdropStyle = useAnimatedStyle(() => ({
-      opacity: navigationProgress.value * curBackdropOpacity.value,
-    }));
-
-    const buttonOpacity = useDerivedValue(() =>
-      interpolate(
-        curBackdropOpacity.value,
-        [0.95, 1],
-        [0, 1],
-        Extrapolate.CLAMP,
-      ),
-    );
-
-    const animatedCloseButtonStyle = useAnimatedStyle(() => {
-      const closeButtonOpacity =
-        navigationProgress.value *
-        buttonOpacity.value *
-        curCloseButtonOpacity.value;
-      return {
-        opacity: closeButtonOpacity,
-      };
-    });
-
-    const animatedMediaIconsButtonStyle = useAnimatedStyle(() => {
-      const actionLinksOpacity =
-        navigationProgress.value *
-        buttonOpacity.value *
-        curActionLinksOpacity.value;
-      return {
-        opacity: actionLinksOpacity,
-      };
-    });
-
-    return (
-      <FullScreenViewModal
-        {...props}
-        dimensions={dimensions}
-        overlayContext={overlayContext}
-        isActive={isActive}
-        closeButtonEnabled={closeButtonEnabled}
-        actionLinksEnabled={actionLinksEnabled}
-        gesture={gesture}
-        closeButtonRef={closeButtonRef}
-        mediaIconsRef={mediaIconsRef}
-        onCloseButtonLayout={onCloseButtonLayout}
-        onMediaIconsLayout={onMediaIconsLayout}
-        close={close}
-        contentViewContainerStyle={contentViewContainerStyle}
-        animatedBackdropStyle={animatedBackdropStyle}
-        animatedCloseButtonStyle={animatedCloseButtonStyle}
-        animatedMediaIconsButtonStyle={animatedMediaIconsButtonStyle}
-      />
-    );
-  });
+const MemoizedFullScreenViewModal: React.ComponentType<Props> =
+  React.memo<Props>(FullScreenViewModal);
 
-export default ConnectedFullScreenViewModal;
+export default MemoizedFullScreenViewModal;