diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js
--- a/native/account/legacy-register-panel.react.js
+++ b/native/account/legacy-register-panel.react.js
@@ -10,7 +10,6 @@
   Keyboard,
   Linking,
 } from 'react-native';
-import Animated from 'react-native-reanimated';
 
 import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js';
 import {
@@ -47,6 +46,7 @@
 import { useSelector } from '../redux/redux-utils.js';
 import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js';
 import type { KeyPressEvent } from '../types/react-native.js';
+import type { ViewStyle } from '../types/styles.js';
 import {
   appOutOfDateAlertDetails,
   usernameReservedAlertDetails,
@@ -64,7 +64,7 @@
 export type LegacyRegisterState = $ReadOnly<WritableLegacyRegisterState>;
 type BaseProps = {
   +setActiveAlert: (activeAlert: boolean) => void,
-  +opacityValue: Animated.Node,
+  +opacityStyle: ViewStyle,
   +legacyRegisterState: StateContainer<LegacyRegisterState>,
 };
 type Props = {
@@ -128,7 +128,7 @@
     );
 
     return (
-      <Panel opacityValue={this.props.opacityValue} style={styles.container}>
+      <Panel opacityStyle={this.props.opacityStyle} style={styles.container}>
         <View style={styles.row}>
           <SWMansionIcon
             name="user-1"
diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js
--- a/native/account/log-in-panel.react.js
+++ b/native/account/log-in-panel.react.js
@@ -3,7 +3,6 @@
 import invariant from 'invariant';
 import * as React from 'react';
 import { View, StyleSheet, Keyboard, Platform } from 'react-native';
-import Animated from 'react-native-reanimated';
 
 import {
   legacyLogInActionTypes,
@@ -47,6 +46,7 @@
 import { useSelector } from '../redux/redux-utils.js';
 import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js';
 import type { KeyPressEvent } from '../types/react-native.js';
+import type { ViewStyle } from '../types/styles.js';
 import {
   appOutOfDateAlertDetails,
   unknownErrorAlertDetails,
@@ -61,7 +61,7 @@
 };
 type BaseProps = {
   +setActiveAlert: (activeAlert: boolean) => void,
-  +opacityValue: Animated.Node,
+  +opacityStyle: ViewStyle,
   +logInState: StateContainer<LogInState>,
 };
 type Props = {
@@ -118,7 +118,7 @@
 
   render(): React.Node {
     return (
-      <Panel opacityValue={this.props.opacityValue}>
+      <Panel opacityStyle={this.props.opacityStyle}>
         <View style={styles.row}>
           <SWMansionIcon
             name="user-1"
diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js
--- a/native/account/logged-out-modal.react.js
+++ b/native/account/logged-out-modal.react.js
@@ -12,7 +12,13 @@
   BackHandler,
   ActivityIndicator,
 } from 'react-native';
-import Animated, { EasingNode } from 'react-native-reanimated';
+import {
+  Easing,
+  useSharedValue,
+  withTiming,
+  useAnimatedStyle,
+  runOnJS,
+} from 'react-native-reanimated';
 import { SafeAreaView } from 'react-native-safe-area-context';
 
 import { setActiveSessionRecoveryActionType } from 'lib/keyserver-conn/keyserver-conn-types.js';
@@ -33,7 +39,7 @@
 import { authoritativeKeyserverID } from '../authoritative-keyserver.js';
 import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js';
 import ConnectedStatusBar from '../connected-status-bar.react.js';
-import { useKeyboardHeight } from '../keyboard/animated-keyboard.js';
+import { useRatchetingKeyboardHeight } from '../keyboard/animated-keyboard.js';
 import { createIsForegroundSelector } from '../navigation/nav-selectors.js';
 import { NavContext } from '../navigation/navigation-context.js';
 import type { RootNavigationProp } from '../navigation/root-navigator.react.js';
@@ -48,55 +54,64 @@
 import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js';
 import { splashStyleSelector } from '../splash.js';
 import { useStyles } from '../themes/colors.js';
-import {
-  runTiming,
-  ratchetAlongWithKeyboardHeight,
-} from '../utils/animation-utils.js';
+import { AnimatedView } from '../types/styles.js';
 import EthereumLogo from '../vectors/ethereum-logo.react.js';
 
 let initialAppLoad = true;
 const safeAreaEdges = ['top', 'bottom'];
 
-const {
-  Value,
-  Node,
-  Clock,
-  block,
-  set,
-  call,
-  cond,
-  not,
-  and,
-  eq,
-  neq,
-  lessThan,
-  greaterOrEq,
-  add,
-  sub,
-  divide,
-  max,
-  stopClock,
-  clockRunning,
-  useValue,
-} = Animated;
-
 export type LoggedOutMode =
   | 'loading'
   | 'prompt'
   | 'log-in'
   | 'register'
   | 'siwe';
-const modeNumbers: { [LoggedOutMode]: number } = {
-  'loading': 0,
-  'prompt': 1,
-  'log-in': 2,
-  'register': 3,
-  'siwe': 4,
+
+const timingConfig = {
+  duration: 250,
+  easing: Easing.out(Easing.ease),
 };
-function isPastPrompt(modeValue: Node) {
-  return and(
-    neq(modeValue, modeNumbers['loading']),
-    neq(modeValue, modeNumbers['prompt']),
+
+// prettier-ignore
+function getPanelPaddingTop(
+  modeValue /*: string */,
+  keyboardHeightValue /*: number */,
+  contentHeightValue /*: number */,
+) /*: number */ {
+  'worklet';
+  const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54;
+  let containerSize = headerHeight;
+  if (modeValue === 'loading' || modeValue === 'prompt') {
+    containerSize += Platform.OS === 'ios' ? 40 : 61;
+  } else if (modeValue === 'log-in') {
+    containerSize += 140;
+  } else if (modeValue === 'register') {
+    containerSize += Platform.OS === 'ios' ? 181 : 180;
+  } else if (modeValue === 'siwe') {
+    containerSize += 250;
+  }
+
+  const freeSpace = contentHeightValue - keyboardHeightValue - containerSize;
+  const targetPanelPaddingTop = Math.max(freeSpace, 0) / 2;
+  return withTiming(targetPanelPaddingTop, timingConfig);
+}
+
+// prettier-ignore
+function getPanelOpacity(
+  modeValue /*: string */,
+  proceedToNextMode /*: () => void */,
+) /*: number */ {
+  'worklet';
+  const targetPanelOpacity =
+    modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1;
+  return withTiming(
+    targetPanelOpacity,
+    timingConfig,
+    (succeeded /*?: boolean */) => {
+      if (succeeded && targetPanelOpacity === 0) {
+        runOnJS(proceedToNextMode)();
+      }
+    },
   );
 }
 
@@ -304,138 +319,21 @@
   const nextModeRef = React.useRef<LoggedOutMode>(initialMode);
 
   const dimensions = useSelector(derivedDimensionsInfoSelector);
-  const contentHeight = useValue(dimensions.safeAreaHeight);
-  const modeValue = useValue(modeNumbers[initialMode]);
-  const buttonOpacity = useValue(persistedStateLoaded ? 1 : 0);
-
-  const [activeAlert, setActiveAlert] = React.useState(false);
-
-  const navContext = React.useContext(NavContext);
-  const isForeground = isForegroundSelector(navContext);
-
-  const keyboardHeightInput = React.useMemo(
-    () => ({
-      ignoreKeyboardDismissal: activeAlert,
-      disabled: !isForeground,
-    }),
-    [activeAlert, isForeground],
-  );
-  const keyboardHeightValue = useKeyboardHeight(keyboardHeightInput);
-
-  const prevModeValue = useValue(modeNumbers[initialMode]);
-  const panelPaddingTop = React.useMemo(() => {
-    const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54;
-    const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61;
-    const logInContainerSize = 140;
-    const registerPanelSize = Platform.OS === 'ios' ? 181 : 180;
-    const siwePanelSize = 250;
-
-    const containerSize = add(
-      headerHeight,
-      cond(not(isPastPrompt(modeValue)), promptButtonsSize, 0),
-      cond(eq(modeValue, modeNumbers['log-in']), logInContainerSize, 0),
-      cond(eq(modeValue, modeNumbers['register']), registerPanelSize, 0),
-      cond(eq(modeValue, modeNumbers['siwe']), siwePanelSize, 0),
-    );
-    const potentialPanelPaddingTop = divide(
-      max(sub(contentHeight, keyboardHeightValue, containerSize), 0),
-      2,
-    );
-
-    const panelPaddingTopValue = new Value(-1);
-    const targetPanelPaddingTop = new Value(-1);
-    const clock = new Clock();
-    const keyboardTimeoutClock = new Clock();
-    return block([
-      cond(lessThan(panelPaddingTopValue, 0), [
-        set(panelPaddingTopValue, potentialPanelPaddingTop),
-        set(targetPanelPaddingTop, potentialPanelPaddingTop),
-      ]),
-      cond(
-        lessThan(keyboardHeightValue, 0),
-        [
-          runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }),
-          cond(
-            not(clockRunning(keyboardTimeoutClock)),
-            set(keyboardHeightValue, 0),
-          ),
-        ],
-        stopClock(keyboardTimeoutClock),
-      ),
-      cond(
-        and(greaterOrEq(keyboardHeightValue, 0), neq(prevModeValue, modeValue)),
-        [
-          stopClock(clock),
-          cond(
-            neq(isPastPrompt(prevModeValue), isPastPrompt(modeValue)),
-            set(targetPanelPaddingTop, potentialPanelPaddingTop),
-          ),
-          set(prevModeValue, modeValue),
-        ],
-      ),
-      ratchetAlongWithKeyboardHeight(keyboardHeightValue, [
-        stopClock(clock),
-        set(targetPanelPaddingTop, potentialPanelPaddingTop),
-      ]),
-      cond(
-        neq(panelPaddingTopValue, targetPanelPaddingTop),
-        set(
-          panelPaddingTopValue,
-          runTiming(clock, panelPaddingTopValue, targetPanelPaddingTop),
-        ),
-      ),
-      panelPaddingTopValue,
-    ]);
-  }, [modeValue, contentHeight, keyboardHeightValue, prevModeValue]);
+  const contentHeight = useSharedValue(dimensions.safeAreaHeight);
+  const modeValue = useSharedValue(initialMode);
+  const buttonOpacity = useSharedValue(persistedStateLoaded ? 1 : 0);
 
   const proceedToNextMode = React.useCallback(() => {
     setMode({ curMode: nextModeRef.current });
   }, [setMode]);
-  const panelOpacity = React.useMemo(() => {
-    const targetPanelOpacity = isPastPrompt(modeValue);
-
-    const panelOpacityValue = new Value(-1);
-    const prevPanelOpacity = new Value(-1);
-    const prevTargetPanelOpacity = new Value(-1);
-    const clock = new Clock();
-    return block([
-      cond(lessThan(panelOpacityValue, 0), [
-        set(panelOpacityValue, targetPanelOpacity),
-        set(prevPanelOpacity, targetPanelOpacity),
-        set(prevTargetPanelOpacity, targetPanelOpacity),
-      ]),
-      cond(greaterOrEq(keyboardHeightValue, 0), [
-        cond(neq(targetPanelOpacity, prevTargetPanelOpacity), [
-          stopClock(clock),
-          set(prevTargetPanelOpacity, targetPanelOpacity),
-        ]),
-        cond(
-          neq(panelOpacityValue, targetPanelOpacity),
-          set(
-            panelOpacityValue,
-            runTiming(clock, panelOpacityValue, targetPanelOpacity),
-          ),
-        ),
-      ]),
-      cond(
-        and(eq(panelOpacityValue, 0), neq(prevPanelOpacity, 0)),
-        call([], proceedToNextMode),
-      ),
-      set(prevPanelOpacity, panelOpacityValue),
-      panelOpacityValue,
-    ]);
-  }, [modeValue, keyboardHeightValue, proceedToNextMode]);
 
   const onPrompt = mode.curMode === 'prompt';
   const prevOnPromptRef = React.useRef(onPrompt);
   React.useEffect(() => {
     if (onPrompt && !prevOnPromptRef.current) {
-      buttonOpacity.setValue(0);
-      Animated.timing(buttonOpacity, {
-        easing: EasingNode.out(EasingNode.ease),
-        duration: 250,
-        toValue: 1.0,
-      }).start();
+      buttonOpacity.value = withTiming(1, {
+        easing: Easing.out(Easing.ease),
+      });
     }
     prevOnPromptRef.current = onPrompt;
   }, [onPrompt, buttonOpacity]);
@@ -447,14 +345,14 @@
       return;
     }
     prevContentHeightRef.current = curContentHeight;
-    contentHeight.setValue(curContentHeight);
+    contentHeight.value = curContentHeight;
   }, [curContentHeight, contentHeight]);
 
   const combinedSetMode = React.useCallback(
     (newMode: LoggedOutMode) => {
       nextModeRef.current = newMode;
       setMode({ curMode: newMode, nextMode: newMode });
-      modeValue.setValue(modeNumbers[newMode]);
+      modeValue.value = newMode;
     },
     [setMode, modeValue],
   );
@@ -462,10 +360,9 @@
   const goBackToPrompt = React.useCallback(() => {
     nextModeRef.current = 'prompt';
     setMode({ nextMode: 'prompt' });
-    keyboardHeightValue.setValue(0);
-    modeValue.setValue(modeNumbers['prompt']);
+    modeValue.value = 'prompt';
     Keyboard.dismiss();
-  }, [setMode, keyboardHeightValue, modeValue]);
+  }, [setMode, modeValue]);
 
   const loadingCompleteRef = React.useRef(persistedStateLoaded);
   React.useEffect(() => {
@@ -475,6 +372,22 @@
     }
   }, [persistedStateLoaded, combinedSetMode]);
 
+  const [activeAlert, setActiveAlert] = React.useState(false);
+
+  const navContext = React.useContext(NavContext);
+  const isForeground = isForegroundSelector(navContext);
+
+  const ratchetingKeyboardHeightInput = React.useMemo(
+    () => ({
+      ignoreKeyboardDismissal: activeAlert,
+      disabled: !isForeground,
+    }),
+    [activeAlert, isForeground],
+  );
+  const keyboardHeightValue = useRatchetingKeyboardHeight(
+    ratchetingKeyboardHeightInput,
+  );
+
   const resetToPrompt = React.useCallback(() => {
     if (nextModeRef.current !== 'prompt') {
       goBackToPrompt();
@@ -532,20 +445,8 @@
   }, [combinedSetMode]);
 
   const onPressLogIn = React.useCallback(() => {
-    if (Platform.OS !== 'ios') {
-      // For some strange reason, iOS's password management logic doesn't
-      // realize that the username and password fields in LogInPanel are related
-      // if the username field gets focused on mount. To avoid this  issue we
-      // need the username and password fields to both appear on-screen before
-      // we focus the username field. However, when we set keyboardHeightValue
-      // to -1 here, we are telling our Reanimated logic to wait until the
-      // keyboard appears before showing LogInPanel. Since we need LogInPanel to
-      // appear before the username field is focused, we need to avoid this
-      // behavior on iOS.
-      keyboardHeightValue.setValue(-1);
-    }
     combinedSetMode('log-in');
-  }, [keyboardHeightValue, combinedSetMode]);
+  }, [combinedSetMode]);
 
   const { navigate } = props.navigation;
   const onPressQRCodeSignIn = React.useCallback(() => {
@@ -553,21 +454,24 @@
   }, [navigate]);
 
   const onPressRegister = React.useCallback(() => {
-    keyboardHeightValue.setValue(-1);
     combinedSetMode('register');
-  }, [keyboardHeightValue, combinedSetMode]);
+  }, [combinedSetMode]);
 
   const onPressNewRegister = React.useCallback(() => {
     navigate(RegistrationRouteName);
   }, [navigate]);
 
+  const opacityStyle = useAnimatedStyle(() => ({
+    opacity: getPanelOpacity(modeValue.value, proceedToNextMode),
+  }));
+
   const styles = useStyles(unboundStyles);
   const panel = React.useMemo(() => {
     if (mode.curMode === 'log-in') {
       return (
         <LogInPanel
           setActiveAlert={setActiveAlert}
-          opacityValue={panelOpacity}
+          opacityStyle={opacityStyle}
           logInState={logInStateContainer}
         />
       );
@@ -575,7 +479,7 @@
       return (
         <LegacyRegisterPanel
           setActiveAlert={setActiveAlert}
-          opacityValue={panelOpacity}
+          opacityStyle={opacityStyle}
           legacyRegisterState={legacyRegisterStateContainer}
         />
       );
@@ -592,7 +496,7 @@
   }, [
     mode.curMode,
     setActiveAlert,
-    panelOpacity,
+    opacityStyle,
     logInStateContainer,
     legacyRegisterStateContainer,
     styles.loadingIndicator,
@@ -615,7 +519,7 @@
     [styles.buttonText, styles.siweButtonText],
   );
   const buttonsViewStyle = React.useMemo(
-    () => [styles.buttonContainer, { opacity: buttonOpacity }],
+    () => [styles.buttonContainer, { opacity: buttonOpacity.value }],
     [styles.buttonContainer, buttonOpacity],
   );
   const buttons = React.useMemo(() => {
@@ -672,7 +576,7 @@
     }
 
     return (
-      <Animated.View style={buttonsViewStyle}>
+      <AnimatedView style={buttonsViewStyle}>
         <LoggedOutStaffInfo />
         <TouchableOpacity
           onPress={onPressSIWE}
@@ -691,7 +595,7 @@
         </View>
         <View style={styles.signInButtons}>{signInButtons}</View>
         <View style={styles.registerButtons}>{registerButtons}</View>
-      </Animated.View>
+      </AnimatedView>
     );
   }, [
     mode.curMode,
@@ -718,28 +622,36 @@
   const backButtonStyle = React.useMemo(
     () => [
       styles.backButton,
-      { opacity: panelOpacity, left: windowWidth < 360 ? 28 : 40 },
+      opacityStyle,
+      { left: windowWidth < 360 ? 28 : 40 },
     ],
-    [styles.backButton, panelOpacity, windowWidth],
+    [styles.backButton, opacityStyle, windowWidth],
   );
 
+  const paddingTopStyle = useAnimatedStyle(() => ({
+    paddingTop: getPanelPaddingTop(
+      modeValue.value,
+      keyboardHeightValue.value,
+      contentHeight.value,
+    ),
+  }));
   const animatedContentStyle = React.useMemo(
-    () => [styles.animationContainer, { paddingTop: panelPaddingTop }],
-    [styles.animationContainer, panelPaddingTop],
+    () => [styles.animationContainer, paddingTopStyle],
+    [styles.animationContainer, paddingTopStyle],
   );
   const animatedContent = React.useMemo(
     () => (
-      <Animated.View style={animatedContentStyle}>
+      <AnimatedView style={animatedContentStyle}>
         <View>
           <Text style={styles.header}>Comm</Text>
-          <Animated.View style={backButtonStyle}>
+          <AnimatedView style={backButtonStyle}>
             <TouchableOpacity activeOpacity={0.6} onPress={resetToPrompt}>
               <Icon name="arrow-circle-o-left" size={36} color="#FFFFFFAA" />
             </TouchableOpacity>
-          </Animated.View>
+          </AnimatedView>
         </View>
         {panel}
-      </Animated.View>
+      </AnimatedView>
     ),
     [
       animatedContentStyle,
diff --git a/native/account/panel-components.react.js b/native/account/panel-components.react.js
--- a/native/account/panel-components.react.js
+++ b/native/account/panel-components.react.js
@@ -9,13 +9,12 @@
   StyleSheet,
   ScrollView,
 } from 'react-native';
-import Animated from 'react-native-reanimated';
 
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 
 import Button from '../components/button.react.js';
 import { useSelector } from '../redux/redux-utils.js';
-import type { ViewStyle } from '../types/styles.js';
+import { type ViewStyle, AnimatedView } from '../types/styles.js';
 
 type ButtonProps = {
   +text: string,
@@ -59,7 +58,7 @@
 }
 
 type PanelProps = {
-  +opacityValue: Animated.Node,
+  +opacityStyle: ViewStyle,
   +children: React.Node,
   +style?: ViewStyle,
 };
@@ -68,17 +67,17 @@
   const containerStyle = React.useMemo(
     () => [
       styles.container,
+      props.opacityStyle,
       {
-        opacity: props.opacityValue,
         marginTop: dimensions.height < 641 ? 15 : 40,
       },
       props.style,
     ],
-    [props.opacityValue, props.style, dimensions.height],
+    [props.opacityStyle, props.style, dimensions.height],
   );
   return (
     <ScrollView bounces={false} keyboardShouldPersistTaps="handled">
-      <Animated.View style={containerStyle}>{props.children}</Animated.View>
+      <AnimatedView style={containerStyle}>{props.children}</AnimatedView>
     </ScrollView>
   );
 }
diff --git a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js
--- a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js
+++ b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js
@@ -545,6 +545,19 @@
 
   declare type CancelAnimation = (animation: number) => void;
 
+  declare type AnimatedKeyboardInfo = {|
+    +height: SharedValue<number>,
+    +state: SharedValue<0 | 1 | 2 | 3 | 4>,
+  |};
+  declare type UseAnimatedKeyboard = (config?: {|
+    +isStatusBarTranslucentAndroid?: boolean,
+  |}) => AnimatedKeyboardInfo;
+
+  declare type UseAnimatedReaction = <T: AnimatableValue>(
+    () => T,
+    (currentValue: T, previousValue: T) => mixed,
+  ) => void;
+
   declare export var Node: typeof NodeImpl;
   declare export var Value: typeof ValueImpl;
   declare export var Clock: typeof ClockImpl;
@@ -599,6 +612,8 @@
   declare export var withTiming: WithTiming;
   declare export var runOnJS: RunOnJS;
   declare export var cancelAnimation: CancelAnimation;
+  declare export var useAnimatedKeyboard: UseAnimatedKeyboard;
+  declare export var useAnimatedReaction: UseAnimatedReaction;
 
   declare export default {
     +Node: typeof NodeImpl,
diff --git a/native/keyboard/animated-keyboard.js b/native/keyboard/animated-keyboard.js
--- a/native/keyboard/animated-keyboard.js
+++ b/native/keyboard/animated-keyboard.js
@@ -3,7 +3,11 @@
 import _isEqual from 'lodash/fp/isEqual.js';
 import * as React from 'react';
 import { Platform } from 'react-native';
-import Reanimated from 'react-native-reanimated';
+import {
+  useSharedValue,
+  type SharedValue,
+  useAnimatedReaction,
+} from 'react-native-reanimated';
 
 import {
   addKeyboardShowListener,
@@ -14,18 +18,18 @@
 import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js';
 import type { KeyboardEvent } from '../types/react-native.js';
 
-const { useValue, Value } = Reanimated;
-
 type UseKeyboardHeightParams = {
   +ignoreKeyboardDismissal?: ?boolean,
   +disabled?: ?boolean,
 };
 
-function useKeyboardHeight(params?: ?UseKeyboardHeightParams): Value {
+function useKeyboardHeight(
+  params?: ?UseKeyboardHeightParams,
+): SharedValue<number> {
   const ignoreKeyboardDismissal = params?.ignoreKeyboardDismissal;
   const disabled = params?.disabled;
 
-  const keyboardHeightValue = useValue(0);
+  const keyboardHeightValue = useSharedValue(0);
 
   const dimensions = useSelector(derivedDimensionsInfoSelector);
   const keyboardShow = React.useCallback(
@@ -44,13 +48,13 @@
           0,
         ),
       });
-      keyboardHeightValue.setValue(keyboardHeight);
+      keyboardHeightValue.value = keyboardHeight;
     },
     [dimensions.bottomInset, keyboardHeightValue],
   );
   const keyboardHide = React.useCallback(() => {
     if (!ignoreKeyboardDismissal) {
-      keyboardHeightValue.setValue(0);
+      keyboardHeightValue.value = 0;
     }
   }, [ignoreKeyboardDismissal, keyboardHeightValue]);
 
@@ -69,4 +73,20 @@
   return keyboardHeightValue;
 }
 
-export { useKeyboardHeight };
+function useRatchetingKeyboardHeight(
+  params?: UseKeyboardHeightParams,
+): SharedValue<number> {
+  const keyboardHeightValue = useKeyboardHeight(params);
+  const ratchetedKeyboardHeight = useSharedValue(0);
+  useAnimatedReaction(
+    () => keyboardHeightValue.value,
+    (currentValue, previousValue) => {
+      if (currentValue > previousValue || currentValue === 0) {
+        ratchetedKeyboardHeight.value = currentValue;
+      }
+    },
+  );
+  return ratchetedKeyboardHeight;
+}
+
+export { useKeyboardHeight, useRatchetingKeyboardHeight };
diff --git a/native/utils/animation-utils.js b/native/utils/animation-utils.js
--- a/native/utils/animation-utils.js
+++ b/native/utils/animation-utils.js
@@ -4,7 +4,6 @@
 import { State as GestureState } from 'react-native-gesture-handler';
 import Animated, {
   EasingNode,
-  type NodeParam,
   type SpringConfig,
   type TimingConfig,
   useSharedValue,
@@ -18,8 +17,6 @@
   block,
   cond,
   not,
-  and,
-  or,
   greaterThan,
   lessThan,
   eq,
@@ -28,7 +25,6 @@
   sub,
   divide,
   set,
-  max,
   startClock,
   stopClock,
   clockRunning,
@@ -152,35 +148,6 @@
   ]);
 }
 
-// You provide a node that performs a "ratchet",
-// and this function will call it as keyboard height increases
-function ratchetAlongWithKeyboardHeight(
-  keyboardHeight: Node,
-  ratchetFunction: NodeParam,
-): Node {
-  const prevKeyboardHeightValue = new Value(-1);
-  // In certain situations, iOS will send multiple keyboardShows in rapid
-  // succession with increasing height values. Only the final value has any
-  // semblance of reality. I've encountered this when using the native
-  // password management integration
-  const whenToUpdate = greaterThan(
-    keyboardHeight,
-    max(prevKeyboardHeightValue, 0),
-  );
-  const whenToReset = and(
-    eq(keyboardHeight, 0),
-    greaterThan(prevKeyboardHeightValue, 0),
-  );
-  return block([
-    cond(
-      lessThan(prevKeyboardHeightValue, 0),
-      set(prevKeyboardHeightValue, keyboardHeight),
-    ),
-    cond(or(whenToUpdate, whenToReset), ratchetFunction),
-    set(prevKeyboardHeightValue, keyboardHeight),
-  ]);
-}
-
 function useSharedValueForBoolean(booleanValue: boolean): SharedValue<boolean> {
   const sharedValue = useSharedValue(booleanValue);
   React.useEffect(() => {
@@ -246,7 +213,6 @@
   gestureJustEnded,
   runTiming,
   runSpring,
-  ratchetAlongWithKeyboardHeight,
   useSharedValueForBoolean,
   animateTowards,
 };