diff --git a/packages/rn-tester/js/examples/Animated/AnimatedIndex.js b/packages/rn-tester/js/examples/Animated/AnimatedIndex.js
index 215efae074c..3fd82fc9c39 100644
--- a/packages/rn-tester/js/examples/Animated/AnimatedIndex.js
+++ b/packages/rn-tester/js/examples/Animated/AnimatedIndex.js
@@ -19,6 +19,7 @@ import EasingExample from './EasingExample';
import FadeInViewExample from './FadeInViewExample';
import LoopingExample from './LoopingExample';
import MovingBoxExample from './MovingBoxExample';
+import PanGestureExample from './PanGestureExample';
import PressabilityWithNativeDrivers from './PressabilityWithNativeDrivers';
import RotatingImagesExample from './RotatingImagesExample';
import TransformBounceExample from './TransformBounceExample';
@@ -47,5 +48,6 @@ export default ({
ContinuousInteractionsExample,
CombineExample,
PressabilityWithNativeDrivers,
+ PanGestureExample,
],
}: RNTesterModule);
diff --git a/packages/rn-tester/js/examples/Animated/PanGestureExample.js b/packages/rn-tester/js/examples/Animated/PanGestureExample.js
new file mode 100644
index 00000000000..7c6dee65cd5
--- /dev/null
+++ b/packages/rn-tester/js/examples/Animated/PanGestureExample.js
@@ -0,0 +1,256 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ * @oncall react_native
+ */
+
+import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
+
+import ToggleNativeDriver from './utils/ToggleNativeDriver';
+import * as React from 'react';
+import {useLayoutEffect, useRef, useState} from 'react';
+import {
+ Animated,
+ Button,
+ PanResponder,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
+
+function TextBox({children}: $ReadOnly<{children: React.Node}>): React.Node {
+ // Prevent touch from being hijacked by Text
+ return (
+
+ {children}
+
+ );
+}
+
+function AnimatedEventExample({
+ containerPageXY,
+ useNativeDriver,
+}: $ReadOnly<{
+ containerPageXY: $ReadOnly<{x: number, y: number}>,
+ useNativeDriver: boolean,
+}>): React.Node {
+ const boxRef = useRef>();
+
+ const pointerPageXY = useRef(
+ new Animated.ValueXY(
+ {
+ x: containerPageXY.x,
+ y: containerPageXY.y,
+ },
+ {useNativeDriver},
+ ),
+ ).current;
+
+ const dragStartOffsetXY = useRef(
+ new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}),
+ ).current;
+
+ // We'll no longer have to subtract containerPageXY.x/y from offset, if we can animate left/top props natively
+ // TODO: T222117268 Adopt fabric driven c++ native animated in RNTester
+ const finalOffsetX = Animated.subtract(
+ Animated.subtract(pointerPageXY.x, dragStartOffsetXY.x),
+ containerPageXY.x,
+ );
+ const finalOffsetY = Animated.subtract(
+ Animated.subtract(pointerPageXY.y, dragStartOffsetXY.y),
+ containerPageXY.y,
+ );
+
+ const syncAnimationToHostView = () => {
+ boxRef.current?.setNativeProps({
+ transform: [
+ {translateX: finalOffsetX.__getValue()},
+ {translateY: finalOffsetY.__getValue()},
+ ],
+ });
+ };
+
+ return (
+ {
+ // Animated change sometimes doesn't commit to Fabric, and box will jump back to offset before animation
+ // This is to make sure that finalOffsetX/Y are synced to native host view
+ // TODO: T222117268 Adopt fabric driven c++ native animated in RNTester
+ syncAnimationToHostView();
+ }}
+ style={[
+ styles.box,
+ {
+ backgroundColor: useNativeDriver ? 'orange' : 'violet',
+ transform: [
+ {
+ translateX: finalOffsetX,
+ },
+ {
+ translateY: finalOffsetY,
+ },
+ ],
+ },
+ ]}>
+ Use {useNativeDriver ? 'Native' : 'JS'} Animated.event
+
+ );
+}
+
+function PanResponderExample({
+ useNativeDriver,
+}: $ReadOnly<{useNativeDriver: boolean}>): React.Node {
+ const finalOffsetXY = useRef(
+ new Animated.ValueXY({x: 0, y: 0}, {useNativeDriver}),
+ ).current;
+ const dragStartOffsetXY = useRef({x: 0, y: 0}).current;
+ const panResponder = useRef(
+ PanResponder.create({
+ onMoveShouldSetPanResponder: (pressEvent, gestureState) => {
+ dragStartOffsetXY.x = finalOffsetXY.x.__getValue();
+ dragStartOffsetXY.y = finalOffsetXY.y.__getValue();
+ return true;
+ },
+ onPanResponderMove: (pressEvent, gestureState) => {
+ if (gestureState.dx !== 0) {
+ finalOffsetXY.x.setValue(dragStartOffsetXY.x + gestureState.dx);
+ }
+
+ if (gestureState.dy !== 0) {
+ finalOffsetXY.y.setValue(dragStartOffsetXY.y + gestureState.dy);
+ }
+ },
+ }),
+ ).current;
+
+ return (
+
+
+ Use PanResponder{' '}
+ {`+ ${useNativeDriver ? 'Native Animated value' : 'JS Animated value'}`}
+
+
+ );
+}
+
+function PanGestureExample(): React.Node {
+ const [busy, setBusy] = useState(false);
+ const [useNativeDriver, setUseNativeDriver] = useState(false);
+
+ const containerRef = useRef>();
+ const [containerPageXY, setContainerPageXY] =
+ useState{x: number, y: number}>(null);
+
+ useLayoutEffect(() => {
+ containerRef.current?.measure((x, y, width, height, pageX, pageY) => {
+ setContainerPageXY({x: pageX, y: pageY});
+ });
+ }, []);
+
+ function sleep(t: number) {
+ setBusy(true);
+ setTimeout(() => {
+ const start = Date.now();
+ while (Date.now() - start < t) {
+ // sleeping
+ }
+ setBusy(false);
+ }, 1000);
+ }
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'stretch',
+ justifyContent: 'flex-start',
+ },
+ examplesContainer: {
+ flex: 1,
+ },
+ box: {
+ width: 160,
+ height: 160,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 12,
+ },
+ text: {
+ pointerEvents: 'none',
+ },
+});
+
+export default ({
+ title: 'Pan Gesture',
+ name: 'panGesture',
+ description: 'Animations driven by pan gesture.',
+ render: () => ,
+}: RNTesterModuleExample);