diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedBackendSuspense-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackendSuspense-itest.js
new file mode 100644
index 000000000000..3cb2553d6e57
--- /dev/null
+++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedBackendSuspense-itest.js
@@ -0,0 +1,592 @@
+/**
+ * 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.
+ *
+ * @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:*
+ * @flow strict-local
+ * @format
+ */
+
+import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
+
+import * as Fantom from '@react-native/fantom';
+import {Suspense, startTransition, use} from 'react';
+import {Animated, Easing, View, useAnimatedValue} from 'react-native';
+import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
+
+// --- Shared test utilities ---
+
+function Fallback({nativeID}: {nativeID?: string}) {
+ return ;
+}
+
+/**
+ * Creates an async data cache backed by manually-resolved promises.
+ * Call `cache.resolveNext()` to resolve the pending fetch.
+ */
+function createDataCache(): {
+ SuspendingChild: React.ComponentType<{dataKey: string}>,
+ useData: (key: string) => string,
+ resolveNext: () => void,
+} {
+ let resolvePromise: (() => void) | null = null;
+ const cache = new Map();
+
+ async function getData(key: string): Promise {
+ await new Promise(resolve => {
+ resolvePromise = resolve;
+ });
+ return `data-${key}`;
+ }
+
+ async function fetchData(key: string): Promise {
+ const data = await getData(key);
+ cache.set(key, data);
+ return data;
+ }
+
+ function useData(key: string): string {
+ let data = cache.get(key);
+ if (data == null) {
+ data = use(fetchData(key));
+ }
+ return data;
+ }
+
+ function SuspendingChild(props: {dataKey: string}) {
+ return ;
+ }
+
+ function resolveNext() {
+ expect(resolvePromise).not.toBeNull();
+ Fantom.runTask(() => {
+ resolvePromise?.();
+ resolvePromise = null;
+ });
+ }
+
+ return {SuspendingChild, useData, resolveNext};
+}
+
+// A promise that never resolves - used to freeze components (react-freeze pattern)
+// $FlowFixMe[incompatible-exact] - Promise types are inexact
+const freezePromise: Promise & {status?: string} = new Promise(() => {});
+freezePromise.status = 'pending';
+
+function Freeze(props: {freeze: boolean, children: React.Node}) {
+ if (props.freeze) {
+ throw freezePromise;
+ }
+ return props.children;
+}
+
+function AnimatedChild({
+ onAnimatedWidth,
+}: {
+ onAnimatedWidth?: (width: Animated.Value) => void,
+}) {
+ const animatedWidth = useAnimatedValue(0);
+ onAnimatedWidth?.(animatedWidth);
+
+ return (
+
+ );
+}
+
+// --- Tests ---
+
+beforeEach(() => {
+ allowStyleProp('width');
+});
+
+test('animation state is maintained after Suspense', () => {
+ let _animatedWidth;
+ let _widthAnimation;
+ const {SuspendingChild, resolveNext} = createDataCache();
+
+ function MyApp(props: {dataKey: string}) {
+ const animatedWidth = useAnimatedValue(0);
+ _animatedWidth = animatedWidth;
+ return (
+
+ }>
+
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ resolveNext();
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ Fantom.runTask(() => {
+ _widthAnimation = Animated.timing(_animatedWidth, {
+ toValue: 100,
+ duration: 1000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(500);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ // Trigger suspense via transition - stale UI should remain visible
+ Fantom.runTask(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ // Animation continues while suspended
+ Fantom.unstable_produceFramesForDuration(250);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ resolveNext();
+
+ // Animation state is maintained after suspense resolves
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+
+ Fantom.unstable_produceFramesForDuration(250);
+
+ Fantom.runTask(() => {
+ _widthAnimation?.stop();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual(
+
+
+ ,
+ );
+});
+
+test('animation continues on animated component during suspenseful transition', () => {
+ let _animatedWidth;
+ let _widthAnimation;
+ const {useData, resolveNext} = createDataCache();
+
+ function AnimatedSuspendingChild(props: {dataKey: string}) {
+ const animatedWidth = useAnimatedValue(0);
+ _animatedWidth = animatedWidth;
+ const data = useData(props.dataKey);
+
+ return (
+
+ );
+ }
+
+ function MyApp(props: {dataKey: string}) {
+ return (
+ }>
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ resolveNext();
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.runTask(() => {
+ _widthAnimation = Animated.timing(_animatedWidth, {
+ toValue: 100,
+ duration: 1000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(500);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Trigger suspense via transition - stale UI should remain visible
+ Fantom.runTask(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(250);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ resolveNext();
+
+ // Animation state is maintained after suspense resolves
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(250);
+
+ Fantom.runTask(() => {
+ _widthAnimation?.stop();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+});
+
+test('animation continues after component is frozen and unfrozen', () => {
+ // This test simulates the react-freeze pattern: a Suspense boundary that throws
+ // a never-resolving promise to "freeze" a component (hiding it but keeping it mounted),
+ // then stops throwing to "unfreeze" it. The animation should continue seamlessly.
+
+ let _animatedWidth;
+ let _widthAnimation;
+
+ function MyApp(props: {frozen: boolean}) {
+ return (
+ }>
+
+ {
+ _animatedWidth = w;
+ }}
+ />
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.runTask(() => {
+ _widthAnimation = Animated.timing(_animatedWidth, {
+ toValue: 100,
+ duration: 1000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(250);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Freeze the component - fallback should be shown
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Animation continues while frozen
+ Fantom.unstable_produceFramesForDuration(250);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Unfreeze - animation state is preserved
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(500);
+
+ Fantom.runTask(() => {
+ _widthAnimation?.stop();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+});
+
+test('animation continues after multiple freeze/unfreeze cycles', () => {
+ let _animatedWidth;
+ let _widthAnimation;
+
+ function MyApp(props: {frozen: boolean}) {
+ return (
+ }>
+
+ {
+ _animatedWidth = w;
+ }}
+ />
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.runTask(() => {
+ _widthAnimation = Animated.timing(_animatedWidth, {
+ toValue: 100,
+ duration: 2000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(200);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // First freeze
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(200);
+
+ // First unfreeze
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(200);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Second freeze
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(400);
+
+ // Second unfreeze
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Third freeze (quick freeze/unfreeze)
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ Fantom.unstable_produceFramesForDuration(100);
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.unstable_produceFramesForDuration(900);
+
+ Fantom.runTask(() => {
+ _widthAnimation?.stop();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+});
+
+test('animation state is maintained when unfrozen after animation completes', () => {
+ let _animatedWidth;
+ let _widthAnimation;
+
+ function MyApp(props: {frozen: boolean}) {
+ return (
+ }>
+
+ {
+ _animatedWidth = w;
+ }}
+ />
+
+
+ );
+ }
+
+ const root = Fantom.createRoot();
+
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ Fantom.runTask(() => {
+ _widthAnimation = Animated.timing(_animatedWidth, {
+ toValue: 100,
+ duration: 1000,
+ easing: Easing.linear,
+ useNativeDriver: true,
+ }).start();
+ });
+
+ Fantom.unstable_produceFramesForDuration(200);
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Freeze the component
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Animation completes while frozen
+ Fantom.unstable_produceFramesForDuration(1000);
+
+ Fantom.runTask(() => {
+ _widthAnimation?.stop();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+
+ // Unfreeze - final animation state is shown
+ Fantom.runTask(() => {
+ root.render();
+ });
+
+ expect(
+ root.getRenderedOutput({props: ['width', 'nativeID']}).toJSX(),
+ ).toEqual();
+});