Claude Skill

mobile-animation-reanimated

React Native Reanimated 4 - shared values, animated styles, spring/timing/decay, layout animations, gesture integration, scroll-driven animations, interpolation, worklets, CSS animations

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-animation-reanimated_skills_mobile-animation-reanimated-3a51ef5.zip · 19 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-animation-reanimated/skills/mobile-animation-reanimated
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Native Reanimated Patterns

Quick Guide: Reanimated 4 is New Architecture only (requires react-native-worklets as a separate dependency). Use useSharedValue + useAnimatedStyle for all animations. Animations run on the UI thread via worklets -- never block the JS thread. Use withSpring (physics-based) or withTiming (duration-based) for transitions, layout animations (entering/exiting) for mount/unmount, and useScrollOffset (renamed from useScrollViewOffset) for scroll-driven animations. Reanimated 4 also introduces CSS animations/transitions as a declarative alternative to the worklet API.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use Animated components (Animated.View, Animated.Text, etc.) for any animated styles -- passing animated styles to regular components causes errors)

(You MUST keep static styles in StyleSheet.create and only animate dynamic properties in useAnimatedStyle -- animating static values wastes UI thread resources)

(You MUST NOT mutate shared values inside useAnimatedStyle callbacks -- read only, or you cause infinite loops)

(You MUST use react-native-worklets as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from react-native-reanimated/plugin to react-native-worklets/plugin)

</critical_requirements>


Auto-detection: Reanimated, react-native-reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, Animated.View, Animated.Text, Animated.ScrollView, entering, exiting, FadeIn, FadeOut, SlideIn, interpolate, interpolateColor, useScrollOffset, GestureDetector, Gesture.Pan, worklet, layout animation, shared value, energyThreshold, CSS animation, react-native-worklets

When to use:

  • Animating view properties (opacity, transforms, colors) on the UI thread
  • Adding entering/exiting animations when components mount/unmount
  • Building gesture-driven animations (drag, swipe, pinch)
  • Creating scroll-driven header collapse, parallax, or sticky effects
  • Interpolating values across ranges (position to opacity, scroll to scale)
  • Implementing spring physics or timing-based transitions

When NOT to use:

  • Simple boolean show/hide without animation (conditional rendering suffices)
  • Static layouts that never change at runtime
  • Animated.Value from React Native core (use Reanimated's shared values instead)

Key patterns covered:

  • Shared values (useSharedValue) + animated styles (useAnimatedStyle)
  • Animation functions: withTiming, withSpring, withDecay
  • Layout animations: entering/exiting with predefined builders
  • Gesture integration: Gesture.Pan + shared values + withDecay
  • Scroll-driven animations with useScrollOffset
  • Interpolation: interpolate and interpolateColor
  • Worklet functions and the 'worklet' directive
  • CSS animations and transitions (Reanimated 4 declarative API)

Detailed Resources:




<red_flags>

RED FLAGS

High Priority Issues:

  • Passing animated styles to regular View/Text instead of Animated.View/Animated.Text -- causes silent failure or crash
  • Mutating shared values inside useAnimatedStyle -- causes infinite re-evaluation loops
  • Using react-native-reanimated/plugin in Babel config with Reanimated 4 -- must use react-native-worklets/plugin (and it must be last in the plugins array)
  • Using Reanimated 4.x with Legacy Architecture (old bridge) -- Reanimated 4 is New Architecture only
  • Animating static properties in useAnimatedStyle instead of keeping them in StyleSheet -- wastes UI thread resources
  • Using restDisplacementThreshold/restSpeedThreshold in withSpring -- removed in v4, replaced by energyThreshold

Medium Priority Issues:

  • Creating layout animation builders inline in render -- allocates objects every render; define outside component or in useMemo
  • Using runOnJS/runOnUI instead of scheduleOnRN/scheduleOnUI -- old API moved to react-native-worklets
  • Missing GestureHandlerRootView at app root -- gestures silently fail without it
  • Using useAnimatedGestureHandler -- removed in v4, migrate to Gesture Handler 2's Gesture API

Gotchas and Edge Cases:

  • withSpring duration: actual completion time = perceptual duration x 1.5 -- divide v3 duration values by 1.5 when migrating
  • useScrollOffset renamed from useScrollViewOffset -- deprecated alias still works temporarily
  • Shared value .value access is synchronous on UI thread but asynchronous on JS thread -- don't rely on immediate reads after writes on JS thread
  • useWorkletCallback removed -- replace with useCallback + 'worklet' directive
  • React Compiler compatibility: use sv.get() and sv.set() instead of direct .value access when using React Compiler
  • combineTransition removed -- use EntryExitTransition.entering(entering).exiting(exiting)
  • Object shared values: reassign the entire object, never mutate individual properties -- mutations break reactivity tracking
  • Removing an animated style from a view does not unset the animated values -- explicitly set properties to undefined to reset
  • On New Architecture, layout animations use nativeID internally -- don't overwrite it on animated components

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST use Animated components (Animated.View, Animated.Text, etc.) for any animated styles -- passing animated styles to regular components causes errors)

(You MUST keep static styles in StyleSheet.create and only animate dynamic properties in useAnimatedStyle -- animating static values wastes UI thread resources)

(You MUST NOT mutate shared values inside useAnimatedStyle callbacks -- read only, or you cause infinite loops)

(You MUST use react-native-worklets as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from react-native-reanimated/plugin to react-native-worklets/plugin)

Failure to follow these rules will cause animation failures, infinite loops, and crashes on the UI thread.

</critical_reminders>

Files (skills)
  • examples
    • core.md 10.3 KB
      # Reanimated - Core Patterns
      
      > Shared values, animated styles, animation functions, and interpolation. See [SKILL.md](../SKILL.md) for decision guidance and red flags.
      
      **Prerequisites:** Reanimated 4 installed with `react-native-worklets`. New Architecture enabled (React Native 0.76+).
      
      ---
      
      ## Pattern 1: Shared Value + Animated Style Basics
      
      ```typescript
      import { useState } from "react";
      import { Pressable, Text, StyleSheet } from "react-native";
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withTiming,
      } from "react-native-reanimated";
      
      const BOX_SIZE = 100;
      const SCALE_ACTIVE = 1.2;
      const SCALE_INACTIVE = 1;
      const OPACITY_ACTIVE = 1;
      const OPACITY_INACTIVE = 0.5;
      const ANIMATION_DURATION = 300;
      
      export function AnimatedBox() {
        const [active, setActive] = useState(false);
        const scale = useSharedValue(SCALE_INACTIVE);
        const opacity = useSharedValue(OPACITY_INACTIVE);
      
        // Only dynamic properties in useAnimatedStyle
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ scale: scale.value }],
          opacity: opacity.value,
        }));
      
        const handlePress = () => {
          const nextActive = !active;
          setActive(nextActive);
          scale.value = withTiming(
            nextActive ? SCALE_ACTIVE : SCALE_INACTIVE,
            { duration: ANIMATION_DURATION }
          );
          opacity.value = withTiming(
            nextActive ? OPACITY_ACTIVE : OPACITY_INACTIVE,
            { duration: ANIMATION_DURATION }
          );
        };
      
        return (
          <Pressable onPress={handlePress}>
            {/* Static styles in StyleSheet, dynamic in animatedStyle */}
            <Animated.View style={[styles.box, animatedStyle]}>
              <Text>{active ? "Active" : "Inactive"}</Text>
            </Animated.View>
          </Pressable>
        );
      }
      
      const styles = StyleSheet.create({
        box: {
          width: BOX_SIZE,
          height: BOX_SIZE,
          backgroundColor: "#3498db",
          borderRadius: 8,
          alignItems: "center",
          justifyContent: "center",
        },
      });
      ```
      
      **Why good:** static styles (width, height, backgroundColor, borderRadius) stay in StyleSheet, only dynamic properties (scale, opacity) in useAnimatedStyle, named constants for all values
      
      ---
      
      ## Pattern 2: React Compiler Compatibility
      
      When using React Compiler, use `get()` and `set()` instead of direct `.value` access to avoid the compiler misinterpreting shared value reactivity.
      
      ```typescript
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withSpring,
      } from "react-native-reanimated";
      
      const TARGET_POSITION = 200;
      const INITIAL_POSITION = 0;
      
      export function CompilerSafeAnimation() {
        const translateX = useSharedValue(INITIAL_POSITION);
      
        // get() instead of .value in animated style
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ translateX: translateX.get() }],
        }));
      
        const handlePress = () => {
          // set() with updater function instead of .value assignment
          translateX.set((current) =>
            current === INITIAL_POSITION ? TARGET_POSITION : INITIAL_POSITION
          );
        };
      
        return (
          <Pressable onPress={handlePress}>
            <Animated.View style={[styles.box, animatedStyle]} />
          </Pressable>
        );
      }
      ```
      
      **Why good:** `get()`/`set()` are React Compiler safe -- direct `.value` access may be misinterpreted by the compiler's memoization analysis
      
      **When to use:** only when your project uses React Compiler. Direct `.value` access remains valid without the compiler.
      
      ---
      
      ## Pattern 3: withSpring Configuration
      
      Two modes: physics-based (damping/stiffness) and duration-based (duration/dampingRatio). Don't mix parameters from both modes.
      
      ```typescript
      import { useSharedValue, withSpring } from "react-native-reanimated";
      
      const TARGET = 100;
      
      // Physics-based: natural feel, bouncy
      // Lower damping = more bouncy, higher stiffness = snappier
      function physicsSpring(sv: ReturnType<typeof useSharedValue<number>>) {
        sv.value = withSpring(TARGET, {
          damping: 80, // how quickly it settles (default: 120)
          stiffness: 600, // how bouncy (default: 900)
          mass: 1, // weight (default: 4)
        });
      }
      
      // Duration-based: predictable timing with spring feel
      // dampingRatio < 1 = bouncy, 1 = critically damped (no overshoot), > 1 = overdamped
      function durationSpring(sv: ReturnType<typeof useSharedValue<number>>) {
        sv.value = withSpring(TARGET, {
          duration: 400, // perceptual duration in ms (default: 550)
          dampingRatio: 0.7, // < 1 bouncy, 1 no bounce, > 1 overdamped
        });
      }
      
      // Clamped spring: prevents overshoot past boundaries
      function clampedSpring(sv: ReturnType<typeof useSharedValue<number>>) {
        sv.value = withSpring(TARGET, {
          duration: 400,
          dampingRatio: 0.7,
          clamp: { min: 0, max: 120 }, // limits movement range
        });
      }
      
      // With completion callback
      function springWithCallback(sv: ReturnType<typeof useSharedValue<number>>) {
        sv.value = withSpring(TARGET, { damping: 100 }, (finished) => {
          "worklet";
          if (finished) {
            // Animation completed -- safe to trigger next animation
          }
        });
      }
      ```
      
      **Why good:** clear separation of physics-based vs duration-based configs, clamp prevents visual overshoot, callback for chaining
      
      **Migration note:** `restDisplacementThreshold` and `restSpeedThreshold` are removed in v4. The new `energyThreshold` (default `6e-9`) is relative to the animation. In most cases, just delete the old threshold parameters.
      
      ---
      
      ## Pattern 4: withTiming and Easing
      
      ```typescript
      import {
        withTiming,
        Easing,
        withSequence,
        withRepeat,
        withDelay,
      } from "react-native-reanimated";
      
      const ANIMATION_DURATION = 300;
      const FADE_DELAY = 200;
      const SHAKE_OFFSET = 10;
      const SHAKE_DURATION = 80;
      const SHAKE_REPETITIONS = 3;
      
      // Basic timing with easing
      function fadeIn(sv: SharedValue<number>) {
        sv.value = withTiming(1, {
          duration: ANIMATION_DURATION,
          easing: Easing.bezierFn(0.25, 0.1, 0.25, 1),
        });
      }
      
      // Delayed animation
      function delayedFade(sv: SharedValue<number>) {
        sv.value = withDelay(
          FADE_DELAY,
          withTiming(1, { duration: ANIMATION_DURATION }),
        );
      }
      
      // Sequence: shake animation
      function shake(sv: SharedValue<number>) {
        sv.value = withSequence(
          withTiming(-SHAKE_OFFSET, { duration: SHAKE_DURATION }),
          withRepeat(
            withTiming(SHAKE_OFFSET, { duration: SHAKE_DURATION }),
            SHAKE_REPETITIONS,
            true, // reverse each iteration
          ),
          withTiming(0, { duration: SHAKE_DURATION }),
        );
      }
      ```
      
      **Easing presets:** `Easing.linear`, `Easing.ease`, `Easing.quad`, `Easing.cubic`, `Easing.bezierFn(x1, y1, x2, y2)`, `Easing.inOut(Easing.quad)`, `Easing.in(Easing.elastic(1))`, `Easing.out(Easing.bounce)`.
      
      ---
      
      ## Pattern 5: withDecay (Momentum)
      
      Simulates friction-based deceleration. Commonly used after gesture fling to maintain momentum.
      
      ```typescript
      import { withDecay, withClamp } from "react-native-reanimated";
      
      const MIN_POSITION = 0;
      const MAX_POSITION = 300;
      
      // Basic decay with velocity from gesture
      function applyDecay(sv: SharedValue<number>, velocity: number) {
        sv.value = withDecay({
          velocity,
          rubberBandEffect: true, // bounces at edges instead of hard stop
          clamp: [MIN_POSITION, MAX_POSITION], // boundaries
        });
      }
      
      // Alternative: withClamp modifier wrapping other animations
      function clampedDecay(sv: SharedValue<number>, velocity: number) {
        sv.value = withClamp(
          { min: MIN_POSITION, max: MAX_POSITION },
          withDecay({ velocity }),
        );
      }
      ```
      
      **Why good:** `rubberBandEffect` gives iOS-like elastic boundary behavior, `clamp` prevents values escaping range
      
      ---
      
      ## Pattern 6: Interpolation
      
      Map value ranges. Use in `useAnimatedStyle` for scroll-driven or gesture-driven transformations.
      
      ```typescript
      import {
        interpolate,
        interpolateColor,
        Extrapolation,
        useAnimatedStyle,
      } from "react-native-reanimated";
      
      const SCROLL_THRESHOLD = 100;
      const HEADER_MAX = 200;
      const HEADER_MIN = 60;
      
      // Number interpolation with clamping
      function useHeaderStyle(scrollY: SharedValue<number>) {
        return useAnimatedStyle(() => {
          const height = interpolate(
            scrollY.value,
            [0, HEADER_MAX - HEADER_MIN],
            [HEADER_MAX, HEADER_MIN],
            Extrapolation.CLAMP,
          );
          const opacity = interpolate(
            scrollY.value,
            [0, SCROLL_THRESHOLD],
            [1, 0],
            Extrapolation.CLAMP,
          );
          return { height, opacity };
        });
      }
      
      // Color interpolation: progress bar changes color
      function useProgressColor(progress: SharedValue<number>) {
        return useAnimatedStyle(() => {
          const backgroundColor = interpolateColor(
            progress.value,
            [0, 0.5, 1],
            ["#FF3B30", "#FFCC00", "#34C759"], // red -> yellow -> green
          );
          return { backgroundColor };
        });
      }
      
      // Mixed left/right extrapolation
      function useParallax(scrollY: SharedValue<number>) {
        return useAnimatedStyle(() => {
          const translateY = interpolate(
            scrollY.value,
            [0, SCROLL_THRESHOLD],
            [0, -50],
            {
              extrapolateLeft: Extrapolation.CLAMP, // don't go past 0
              extrapolateRight: Extrapolation.EXTEND, // continue beyond range
            },
          );
          return { transform: [{ translateY }] };
        });
      }
      ```
      
      **Extrapolation types:**
      
      | Type       | Behavior                      |
      | ---------- | ----------------------------- |
      | `CLAMP`    | Caps output at range edges    |
      | `EXTEND`   | Continues linearly past range |
      | `IDENTITY` | Returns the raw input value   |
      
      **`interpolateColor` modes:** `'RGB'` (default) or `'HSV'`. HSV produces perceptually smoother hue transitions (e.g., red to blue through purple, not through muddy brown).
      
      ---
      
      ## Pattern 7: Animated Text and Animated.createAnimatedComponent
      
      Not all components have `Animated.*` wrappers. Use `Animated.createAnimatedComponent` for custom components.
      
      ```typescript
      import Animated from "react-native-reanimated";
      import { TextInput } from "react-native";
      
      // Built-in: Animated.View, Animated.Text, Animated.ScrollView, Animated.Image, Animated.FlatList
      // Custom: wrap any component that accepts style prop
      const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
      
      // Usage
      function AnimatedSearch() {
        const width = useSharedValue(200);
      
        const style = useAnimatedStyle(() => ({
          width: width.value,
        }));
      
        return <AnimatedTextInput style={[styles.input, style]} placeholder="Search..." />;
      }
      ```
      
      **Why good:** extends Reanimated to any component that accepts a `style` prop, type-safe with the wrapped component's props
      
      **Gotcha:** `createAnimatedComponent` should be called at module level, not inside a component -- calling it in render creates a new component type each time, breaking React reconciliation.
      
    • gestures.md 7.4 KB
      # Reanimated - Gesture Integration
      
      > Gesture-driven animations using react-native-gesture-handler + Reanimated shared values. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related:** [core.md](core.md) for shared values and withDecay.
      
      **Note:** This file covers Reanimated's side of gesture-driven animations. For gesture configuration details (tap, pinch, fling, simultaneous/exclusive gestures), see the gesture handler skill.
      
      ---
      
      ## Pattern 1: Draggable Element with Pan Gesture
      
      ```typescript
      import { StyleSheet } from "react-native";
      import { Gesture, GestureDetector } from "react-native-gesture-handler";
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withSpring,
      } from "react-native-reanimated";
      
      const INITIAL_POSITION = 0;
      
      export function DraggableCircle() {
        const translateX = useSharedValue(INITIAL_POSITION);
        const translateY = useSharedValue(INITIAL_POSITION);
        const savedX = useSharedValue(INITIAL_POSITION);
        const savedY = useSharedValue(INITIAL_POSITION);
      
        const pan = Gesture.Pan()
          .onStart(() => {
            // Save position at gesture start for relative movement
            savedX.value = translateX.value;
            savedY.value = translateY.value;
          })
          .onUpdate((e) => {
            // Absolute translation from gesture start
            translateX.value = savedX.value + e.translationX;
            translateY.value = savedY.value + e.translationY;
          })
          .onEnd(() => {
            // Spring back to origin
            translateX.value = withSpring(INITIAL_POSITION);
            translateY.value = withSpring(INITIAL_POSITION);
          });
      
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [
            { translateX: translateX.value },
            { translateY: translateY.value },
          ],
        }));
      
        return (
          <GestureDetector gesture={pan}>
            <Animated.View style={[styles.circle, animatedStyle]} />
          </GestureDetector>
        );
      }
      
      const styles = StyleSheet.create({
        circle: {
          width: 80,
          height: 80,
          borderRadius: 40,
          backgroundColor: "#3498db",
        },
      });
      ```
      
      **Why good:** `onStart` saves position for relative movement, `onUpdate` uses absolute translationX/Y from gesture start, `onEnd` springs back, gesture callbacks are auto-workletized
      
      ### onUpdate vs onChange
      
      - **`onUpdate`** -- receives absolute values (`translationX`, `translationY`) relative to gesture start
      - **`onChange`** -- receives delta values (`changeX`, `changeY`) since last event
      
      Use `onChange` when accumulating position incrementally. Use `onUpdate` when setting position from a known start point.
      
      ---
      
      ## Pattern 2: Swipe-to-Dismiss
      
      ```typescript
      import { Dimensions, StyleSheet, Text } from "react-native";
      import { Gesture, GestureDetector } from "react-native-gesture-handler";
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withTiming,
        interpolate,
        Extrapolation,
        type SharedValue,
      } from "react-native-reanimated";
      import { scheduleOnRN } from "react-native-worklets";
      
      const SCREEN_WIDTH = Dimensions.get("window").width;
      const DISMISS_THRESHOLD = SCREEN_WIDTH * 0.3;
      const DISMISS_VELOCITY = 500;
      const ANIMATION_DURATION = 200;
      
      interface SwipeableCardProps {
        onDismiss: () => void;
        children: React.ReactNode;
      }
      
      export function SwipeableCard({ onDismiss, children }: SwipeableCardProps) {
        const translateX = useSharedValue(0);
      
        const pan = Gesture.Pan()
          .activeOffsetX([-10, 10])  // require 10px horizontal movement to activate
          .onChange((e) => {
            translateX.value += e.changeX;
          })
          .onFinalize((e) => {
            const shouldDismiss =
              Math.abs(translateX.value) > DISMISS_THRESHOLD ||
              Math.abs(e.velocityX) > DISMISS_VELOCITY;
      
            if (shouldDismiss) {
              const direction = translateX.value > 0 ? 1 : -1;
              translateX.value = withTiming(
                direction * SCREEN_WIDTH,
                { duration: ANIMATION_DURATION },
                () => {
                  // scheduleOnRN to call JS function from UI thread
                  scheduleOnRN(onDismiss);
                }
              );
            } else {
              // Snap back
              translateX.value = withTiming(0, { duration: ANIMATION_DURATION });
            }
          });
      
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ translateX: translateX.value }],
          opacity: interpolate(
            Math.abs(translateX.value),
            [0, SCREEN_WIDTH],
            [1, 0.5],
            Extrapolation.CLAMP
          ),
        }));
      
        return (
          <GestureDetector gesture={pan}>
            <Animated.View style={[styles.card, animatedStyle]}>
              {children}
            </Animated.View>
          </GestureDetector>
        );
      }
      
      const styles = StyleSheet.create({
        card: {
          padding: 16,
          backgroundColor: "#FFFFFF",
          borderRadius: 12,
          shadowColor: "#000",
          shadowOffset: { width: 0, height: 2 },
          shadowOpacity: 0.1,
          shadowRadius: 4,
          elevation: 3,
        },
      });
      ```
      
      **Why good:** `activeOffsetX` prevents accidental activation during vertical scroll, velocity check catches fast flicks, `scheduleOnRN` bridges from UI thread callback to JS, opacity fades with distance
      
      **Key points:**
      
      - `scheduleOnRN` (Reanimated 4) replaces `runOnJS` for calling JS functions from worklets
      - `onFinalize` fires when gesture ends regardless of success/failure -- safer than `onEnd` for cleanup
      - `activeOffsetX` with symmetric values prevents horizontal pan from stealing vertical scroll
      
      ---
      
      ## Pattern 3: Gesture with Decay (Fling Momentum)
      
      ```typescript
      import { Gesture, GestureDetector } from "react-native-gesture-handler";
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withDecay,
      } from "react-native-reanimated";
      
      const MIN_X = 0;
      const MAX_X = 300;
      
      export function FlickableElement() {
        const offsetX = useSharedValue(0);
      
        const pan = Gesture.Pan()
          .onChange((e) => {
            offsetX.value += e.changeX;
          })
          .onFinalize((e) => {
            // withDecay preserves momentum from the fling
            offsetX.value = withDecay({
              velocity: e.velocityX,
              rubberBandEffect: true,    // elastic bounce at boundaries
              clamp: [MIN_X, MAX_X],    // movement boundaries
            });
          });
      
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ translateX: offsetX.value }],
        }));
      
        return (
          <GestureDetector gesture={pan}>
            <Animated.View style={[styles.element, animatedStyle]} />
          </GestureDetector>
        );
      }
      ```
      
      **Why good:** `withDecay` takes velocity directly from gesture event, `rubberBandEffect` gives iOS-like elastic boundary, `clamp` prevents element from escaping bounds
      
      ---
      
      ## Pattern 4: Scale on Press (Tap Gesture)
      
      ```typescript
      import { Gesture, GestureDetector } from "react-native-gesture-handler";
      import Animated, {
        useSharedValue,
        useAnimatedStyle,
        withSpring,
      } from "react-native-reanimated";
      
      const SCALE_PRESSED = 0.95;
      const SCALE_DEFAULT = 1;
      
      export function PressableCard({ children }: { children: React.ReactNode }) {
        const scale = useSharedValue(SCALE_DEFAULT);
      
        const tap = Gesture.Tap()
          .onBegin(() => {
            scale.value = withSpring(SCALE_PRESSED, { damping: 100, stiffness: 800 });
          })
          .onFinalize(() => {
            scale.value = withSpring(SCALE_DEFAULT, { damping: 100, stiffness: 800 });
          });
      
        const animatedStyle = useAnimatedStyle(() => ({
          transform: [{ scale: scale.value }],
        }));
      
        return (
          <GestureDetector gesture={tap}>
            <Animated.View style={[styles.card, animatedStyle]}>
              {children}
            </Animated.View>
          </GestureDetector>
        );
      }
      ```
      
      **Why good:** `onBegin` fires immediately on touch (not on release), `onFinalize` restores scale whether tap completes or cancels, spring gives natural bounce feel
      
    • layout-animations.md 6.3 KB
      # Reanimated - Layout Animations
      
      > Entering/exiting animations, predefined builders, custom animations. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related:** [core.md](core.md) for shared values and animation functions.
      
      ---
      
      ## Pattern 1: Predefined Entering/Exiting
      
      Apply to `Animated.*` components. Component must be conditionally rendered (mount/unmount triggers the animation).
      
      ```typescript
      import { useState } from "react";
      import { Pressable, Text, StyleSheet, View } from "react-native";
      import Animated, {
        FadeIn,
        FadeOut,
        SlideInRight,
        SlideOutLeft,
        ZoomIn,
        BounceIn,
      } from "react-native-reanimated";
      
      const ANIMATION_DURATION = 400;
      const STAGGER_DELAY = 100;
      
      export function NotificationList({ items }: { items: Notification[] }) {
        return (
          <View style={styles.list}>
            {items.map((item, index) => (
              <Animated.View
                key={item.id}
                entering={SlideInRight.delay(index * STAGGER_DELAY).duration(ANIMATION_DURATION)}
                exiting={SlideOutLeft.duration(ANIMATION_DURATION)}
                style={styles.item}
              >
                <Text>{item.message}</Text>
              </Animated.View>
            ))}
          </View>
        );
      }
      
      const styles = StyleSheet.create({
        list: { gap: 8 },
        item: {
          padding: 16,
          backgroundColor: "#F0F0F0",
          borderRadius: 8,
        },
      });
      ```
      
      **Why good:** staggered delay based on index creates cascade effect, separate entering/exiting animations for different UX feel
      
      ### Available Builder Families
      
      | Family     | Entering            | Exiting              | Variants                                |
      | ---------- | ------------------- | -------------------- | --------------------------------------- |
      | Fade       | `FadeIn`            | `FadeOut`            | Up, Down, Left, Right                   |
      | Slide      | `SlideInRight`      | `SlideOutLeft`       | Up, Down, Left, Right                   |
      | Zoom       | `ZoomIn`            | `ZoomOut`            | Up, Down, Left, Right, EasyUp, EasyDown |
      | Bounce     | `BounceIn`          | `BounceOut`          | Up, Down, Left, Right                   |
      | Flip       | `FlipInEasyX`       | `FlipOutEasyX`       | X, Y, XUp, XDown, YLeft, YRight         |
      | LightSpeed | `LightSpeedInRight` | `LightSpeedOutRight` | Left, Right                             |
      | Rotate     | `RotateInDownLeft`  | `RotateOutDownLeft`  | Up/Down + Left/Right                    |
      | Pinwheel   | `PinwheelIn`        | `PinwheelOut`        | (no variants)                           |
      | Roll       | `RollInLeft`        | `RollOutLeft`        | Left, Right                             |
      | Stretch    | `StretchInX`        | `StretchOutX`        | X, Y                                    |
      
      ---
      
      ## Pattern 2: Modifier Chaining
      
      Customize predefined animations with modifier methods.
      
      ```typescript
      import Animated, { FadeIn, SlideInUp, BounceIn } from "react-native-reanimated";
      
      const ENTER_DURATION = 500;
      const EXIT_DURATION = 300;
      const ENTER_DELAY = 200;
      
      // Duration + easing
      <Animated.View entering={FadeIn.duration(ENTER_DURATION).easing(Easing.bezierFn(0.25, 0.1, 0.25, 1))} />
      
      // Spring-based (replaces default timing with spring physics)
      <Animated.View entering={BounceIn.springify().damping(12).stiffness(200)} />
      
      // Delay + callback
      <Animated.View
        entering={SlideInUp.delay(ENTER_DELAY).withCallback((finished) => {
          "worklet";
          if (finished) {
            // Animation completed on UI thread
          }
        })}
      />
      
      // Override initial values
      <Animated.View
        entering={FadeIn.withInitialValues({ opacity: 0.3, transform: [{ scale: 0.8 }] })}
      />
      
      // Accessibility: respect reduced motion preference
      <Animated.View entering={FadeIn.reduceMotion(ReduceMotion.System)} />
      ```
      
      **Performance tip:** Define builders outside the component or memoize them -- inline creation in JSX allocates new objects every render.
      
      ```typescript
      // Good: defined once at module level
      const ENTER_ANIMATION = FadeIn.duration(400).delay(100);
      const EXIT_ANIMATION = FadeOut.duration(300);
      
      function Card() {
        return <Animated.View entering={ENTER_ANIMATION} exiting={EXIT_ANIMATION} />;
      }
      ```
      
      ---
      
      ## Pattern 3: EntryExitTransition (Replacing combineTransition)
      
      `combineTransition` was removed in v4. Use `EntryExitTransition` to combine different entering and exiting animations into a single layout transition.
      
      ```typescript
      import Animated, {
        FadeIn,
        FadeOut,
        SlideInRight,
        SlideOutLeft,
        EntryExitTransition,
      } from "react-native-reanimated";
      
      // Combine different entering and exiting animations
      const LAYOUT_TRANSITION = EntryExitTransition
        .entering(SlideInRight)
        .exiting(FadeOut);
      
      function AnimatedList({ items }: { items: Item[] }) {
        return (
          <View>
            {items.map((item) => (
              <Animated.View
                key={item.id}
                layout={LAYOUT_TRANSITION}
                style={styles.item}
              >
                <Text>{item.name}</Text>
              </Animated.View>
            ))}
          </View>
        );
      }
      ```
      
      ---
      
      ## Pattern 4: Custom Entering/Exiting Animation
      
      Build fully custom animations when predefined builders don't fit.
      
      ```typescript
      import Animated, { withTiming, withSpring, type EntryAnimationsValues } from "react-native-reanimated";
      
      const CUSTOM_DURATION = 600;
      const INITIAL_OFFSET = 50;
      
      // Custom entering animation function
      function customEnteringAnimation(values: EntryAnimationsValues) {
        "worklet";
        const animations = {
          opacity: withTiming(1, { duration: CUSTOM_DURATION }),
          transform: [
            { translateY: withSpring(0, { damping: 100 }) },
            { scale: withSpring(1, { damping: 80 }) },
          ],
        };
        const initialValues = {
          opacity: 0,
          transform: [
            { translateY: INITIAL_OFFSET },
            { scale: 0.9 },
          ],
        };
        return { initialValues, animations };
      }
      
      function CustomAnimatedCard() {
        return (
          <Animated.View entering={customEnteringAnimation} style={styles.card}>
            <Text>Custom animation</Text>
          </Animated.View>
        );
      }
      ```
      
      **Why good:** full control over initial values and animation config per property, can mix spring and timing in one entering animation, `values` parameter provides target layout measurements (width, height, targetOriginX, etc.)
      
      **Custom animation callback shape:**
      
      - Receives `EntryAnimationsValues` (entering) or `ExitAnimationsValues` (exiting) with target layout info
      - Must return `{ initialValues, animations }` where each has the same style property keys
      - Must be marked with `'worklet'` directive
      
    • scroll-animations.md 7 KB
      # Reanimated - Scroll-Driven Animations
      
      > Scroll-driven animations with useScrollOffset, collapsing headers, parallax. See [SKILL.md](../SKILL.md) for decision guidance.
      
      **Related:** [core.md](core.md) for interpolation patterns.
      
      ---
      
      ## Pattern 1: Collapsing Header
      
      Track scroll offset and interpolate header height, title opacity, and background.
      
      ```typescript
      import { StyleSheet, Text, View } from "react-native";
      import Animated, {
        useAnimatedRef,
        useScrollOffset,
        useAnimatedStyle,
        interpolate,
        interpolateColor,
        Extrapolation,
      } from "react-native-reanimated";
      
      const HEADER_MAX_HEIGHT = 200;
      const HEADER_MIN_HEIGHT = 60;
      const SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
      const TITLE_FONT_MAX = 28;
      const TITLE_FONT_MIN = 18;
      
      export function CollapsibleHeaderScreen() {
        const scrollRef = useAnimatedRef<Animated.ScrollView>();
        const scrollOffset = useScrollOffset(scrollRef);
      
        const headerStyle = useAnimatedStyle(() => ({
          height: interpolate(
            scrollOffset.value,
            [0, SCROLL_DISTANCE],
            [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
            Extrapolation.CLAMP
          ),
          backgroundColor: interpolateColor(
            scrollOffset.value,
            [0, SCROLL_DISTANCE],
            ["rgba(0,0,0,0)", "rgba(0,0,0,0.9)"]
          ),
        }));
      
        const titleStyle = useAnimatedStyle(() => ({
          fontSize: interpolate(
            scrollOffset.value,
            [0, SCROLL_DISTANCE],
            [TITLE_FONT_MAX, TITLE_FONT_MIN],
            Extrapolation.CLAMP
          ),
          opacity: interpolate(
            scrollOffset.value,
            [0, SCROLL_DISTANCE * 0.5, SCROLL_DISTANCE],
            [1, 0.8, 1],
            Extrapolation.CLAMP
          ),
        }));
      
        return (
          <View style={styles.container}>
            <Animated.View style={[styles.header, headerStyle]}>
              <Animated.Text style={[styles.title, titleStyle]}>
                Profile
              </Animated.Text>
            </Animated.View>
      
            <Animated.ScrollView
              ref={scrollRef}
              contentContainerStyle={{ paddingTop: HEADER_MAX_HEIGHT }}
              scrollEventThrottle={16}
            >
              {/* Scroll content */}
            </Animated.ScrollView>
          </View>
        );
      }
      
      const styles = StyleSheet.create({
        container: { flex: 1 },
        header: {
          position: "absolute",
          top: 0,
          left: 0,
          right: 0,
          zIndex: 1,
          justifyContent: "flex-end",
          paddingHorizontal: 16,
          paddingBottom: 12,
        },
        title: {
          color: "#FFFFFF",
          fontWeight: "bold",
        },
      });
      ```
      
      **Why good:** `useScrollOffset` auto-detects scroll direction, `Extrapolation.CLAMP` prevents over/under-shoot, font size and opacity give depth to the collapse
      
      **Key detail:** `scrollEventThrottle={16}` is needed on `ScrollView` for smooth 60fps tracking. Without it, scroll events fire less frequently.
      
      ---
      
      ## Pattern 2: Parallax Image
      
      Image moves at a slower rate than content, creating depth.
      
      ```typescript
      import { StyleSheet, Text, View, Dimensions } from "react-native";
      import Animated, {
        useAnimatedRef,
        useScrollOffset,
        useAnimatedStyle,
        interpolate,
        Extrapolation,
      } from "react-native-reanimated";
      
      const IMAGE_HEIGHT = 300;
      const PARALLAX_FACTOR = 0.5;  // image moves at half the scroll speed
      
      export function ParallaxScreen() {
        const scrollRef = useAnimatedRef<Animated.ScrollView>();
        const scrollOffset = useScrollOffset(scrollRef);
      
        const imageStyle = useAnimatedStyle(() => ({
          transform: [
            {
              translateY: interpolate(
                scrollOffset.value,
                [0, IMAGE_HEIGHT],
                [0, IMAGE_HEIGHT * PARALLAX_FACTOR],
                Extrapolation.CLAMP
              ),
            },
          ],
        }));
      
        const overlayStyle = useAnimatedStyle(() => ({
          opacity: interpolate(
            scrollOffset.value,
            [0, IMAGE_HEIGHT * 0.5],
            [0, 0.7],
            Extrapolation.CLAMP
          ),
        }));
      
        return (
          <Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}>
            <View style={styles.imageContainer}>
              <Animated.Image
                source={{ uri: "https://example.com/hero.jpg" }}
                style={[styles.image, imageStyle]}
              />
              <Animated.View style={[styles.overlay, overlayStyle]} />
            </View>
            <View style={styles.content}>
              <Text>Content below parallax image</Text>
            </View>
          </Animated.ScrollView>
        );
      }
      
      const styles = StyleSheet.create({
        imageContainer: {
          height: IMAGE_HEIGHT,
          overflow: "hidden",
        },
        image: {
          width: "100%",
          height: IMAGE_HEIGHT * 1.5,  // taller than container for parallax room
        },
        overlay: {
          ...StyleSheet.absoluteFillObject,
          backgroundColor: "#000000",
        },
        content: {
          padding: 16,
          backgroundColor: "#FFFFFF",
        },
      });
      ```
      
      **Why good:** `PARALLAX_FACTOR` controls depth effect intensity, image is taller than container for scroll room, overlay fades in as image scrolls away
      
      ---
      
      ## Pattern 3: Scroll-to-Hide Tab Bar
      
      Tab bar slides down and fades out when scrolling down, reappears when scrolling up.
      
      ```typescript
      import { StyleSheet, View, Text } from "react-native";
      import Animated, {
        useAnimatedRef,
        useScrollOffset,
        useSharedValue,
        useAnimatedStyle,
        withTiming,
        interpolate,
        Extrapolation,
      } from "react-native-reanimated";
      
      const TAB_BAR_HEIGHT = 60;
      const SCROLL_THRESHOLD = 10;
      const ANIMATION_DURATION = 200;
      
      export function HidingTabBar() {
        const scrollRef = useAnimatedRef<Animated.ScrollView>();
        const scrollOffset = useScrollOffset(scrollRef);
        const prevOffset = useSharedValue(0);
        const tabBarTranslateY = useSharedValue(0);
      
        // Detect scroll direction and animate tab bar
        const animatedTabStyle = useAnimatedStyle(() => {
          const diff = scrollOffset.value - prevOffset.value;
          prevOffset.value = scrollOffset.value;
      
          if (diff > SCROLL_THRESHOLD) {
            // Scrolling down: hide
            tabBarTranslateY.value = withTiming(TAB_BAR_HEIGHT, {
              duration: ANIMATION_DURATION,
            });
          } else if (diff < -SCROLL_THRESHOLD) {
            // Scrolling up: show
            tabBarTranslateY.value = withTiming(0, {
              duration: ANIMATION_DURATION,
            });
          }
      
          return {
            transform: [{ translateY: tabBarTranslateY.value }],
            opacity: interpolate(
              tabBarTranslateY.value,
              [0, TAB_BAR_HEIGHT],
              [1, 0],
              Extrapolation.CLAMP
            ),
          };
        });
      
        return (
          <View style={styles.container}>
            <Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}>
              {/* content */}
            </Animated.ScrollView>
      
            <Animated.View style={[styles.tabBar, animatedTabStyle]}>
              <Text>Tab 1</Text>
              <Text>Tab 2</Text>
              <Text>Tab 3</Text>
            </Animated.View>
          </View>
        );
      }
      
      const styles = StyleSheet.create({
        container: { flex: 1 },
        tabBar: {
          position: "absolute",
          bottom: 0,
          left: 0,
          right: 0,
          height: TAB_BAR_HEIGHT,
          flexDirection: "row",
          justifyContent: "space-around",
          alignItems: "center",
          backgroundColor: "#FFFFFF",
          borderTopWidth: 1,
          borderTopColor: "#E0E0E0",
        },
      });
      ```
      
      **Why good:** threshold prevents jittery show/hide on small scrolls, opacity interpolated from translateY for coordinated animation, previous offset tracked to detect direction
      
  • reference.md 7.6 KB
    # Reanimated Quick Reference
    
    > Decision frameworks, migration guide, and API quick reference. See [SKILL.md](SKILL.md) for red flags and patterns.
    
    ---
    
    ## Decision Framework
    
    ### Which Animation Function?
    
    ```
    What drives the animation?
    |
    +-> State change (boolean toggle, prop change)?
    |   +-> Simple transition? -> CSS transitions (Reanimated 4) or withTiming
    |   +-> Need spring feel? -> withSpring
    |   +-> Complex keyframes? -> CSS animations (Reanimated 4) or withSequence
    |
    +-> User gesture (drag, swipe, pinch)?
    |   +-> During gesture? -> Direct shared value update in gesture callback
    |   +-> After gesture release? -> withDecay (momentum) or withSpring (snap back)
    |
    +-> Scroll position?
    |   +-> useScrollOffset + interpolate in useAnimatedStyle
    |
    +-> Component mount/unmount?
        +-> Predefined? -> entering={FadeIn} exiting={FadeOut}
        +-> Custom? -> entering={customFn} with 'worklet' directive
    ```
    
    ### CSS Animations vs Worklets
    
    ```
    Which API to use?
    |
    +-> State-driven (toggle, prop change)?
    |   +-> CSS transitions/animations (less code, better optimizable)
    |
    +-> Gesture-driven (drag, swipe)?
    |   +-> Worklet API (shared values + gesture callbacks)
    |
    +-> Scroll-driven (parallax, collapse)?
    |   +-> Worklet API (useScrollOffset + interpolate)
    |
    +-> Frame-by-frame control needed?
    |   +-> Worklet API (full control)
    |
    +-> Mixed?
        +-> Both work simultaneously -- use CSS for simple parts, worklets for complex
    ```
    
    ### withSpring Configuration
    
    ```
    What kind of spring behavior?
    |
    +-> Need precise timing control?
    |   +-> Duration-based: { duration: 500, dampingRatio: 0.8 }
    |   +-> dampingRatio < 1 = bouncy, 1 = no overshoot, > 1 = overdamped
    |
    +-> Need natural physics feel?
    |   +-> Physics-based: { damping: 100, stiffness: 800 }
    |   +-> Lower damping = more bouncy, higher stiffness = snappier
    |
    +-> Need to prevent overshoot?
    |   +-> Add overshootClamping: true
    |   +-> Or use clamp: { min: 0, max: 200 }
    |
    +-> Don't mix physics-based and duration-based params
    ```
    
    ---
    
    ## Migration from 3.x to 4.x
    
    ### Dependency Changes
    
    | Step         | 3.x                              | 4.x                                                 |
    | ------------ | -------------------------------- | --------------------------------------------------- |
    | Install      | `react-native-reanimated`        | `react-native-reanimated` + `react-native-worklets` |
    | Babel plugin | `react-native-reanimated/plugin` | `react-native-worklets/plugin` (must be last)       |
    | Architecture | Both old and new                 | New Architecture only                               |
    
    ### API Renames
    
    | 3.x                                 | 4.x                                | Package                   |
    | ----------------------------------- | ---------------------------------- | ------------------------- |
    | `runOnJS(fn)("arg")`                | `scheduleOnRN(fn, "arg")`          | `react-native-worklets`   |
    | `runOnUI(fn)("arg")`                | `scheduleOnUI(fn, "arg")`          | `react-native-worklets`   |
    | `executeOnUIRuntimeSync(fn)("arg")` | `runOnUISync(fn, "arg")`           | `react-native-worklets`   |
    | `runOnRuntime(rt, fn)("arg")`       | `scheduleOnRuntime(rt, fn, "arg")` | `react-native-worklets`   |
    | `useScrollViewOffset`               | `useScrollOffset`                  | `react-native-reanimated` |
    
    ### Removed APIs
    
    | Removed                     | Replacement                                  |
    | --------------------------- | -------------------------------------------- |
    | `useWorkletCallback`        | `useCallback` + `'worklet'` directive        |
    | `useAnimatedGestureHandler` | Gesture Handler 2 `Gesture` API              |
    | `combineTransition`         | `EntryExitTransition.entering(X).exiting(Y)` |
    | `addWhitelistedNativeProps` | Remove (no-op in v4)                         |
    | `addWhitelistedUIProps`     | Remove (no-op in v4)                         |
    | `restDisplacementThreshold` | `energyThreshold` (or just remove)           |
    | `restSpeedThreshold`        | `energyThreshold` (or just remove)           |
    
    ### withSpring Duration Change
    
    Actual completion time = perceptual `duration` x 1.5. To get equivalent timing from v3, divide duration by 1.5.
    
    ---
    
    ## API Quick Reference
    
    ### Core Hooks
    
    | Hook                            | Purpose                                        |
    | ------------------------------- | ---------------------------------------------- |
    | `useSharedValue(initial)`       | Create reactive value on UI thread             |
    | `useAnimatedStyle(() => style)` | Derive animated styles from shared values      |
    | `useAnimatedRef()`              | Create ref for scroll offset tracking          |
    | `useScrollOffset(ref)`          | Track scroll position as shared value          |
    | `useDerivedValue(() => expr)`   | Compute derived value from shared values       |
    | `useAnimatedProps(() => props)` | Animate non-style props (e.g., SVG attributes) |
    
    ### Animation Functions
    
    | Function                 | Purpose        | Key Config                                         |
    | ------------------------ | -------------- | -------------------------------------------------- |
    | `withTiming(to, config)` | Duration-based | `duration`, `easing`                               |
    | `withSpring(to, config)` | Physics spring | `damping`/`stiffness` or `duration`/`dampingRatio` |
    | `withDecay(config)`      | Momentum       | `velocity`, `clamp`, `rubberBandEffect`            |
    
    ### Modifiers
    
    | Modifier                                | Purpose                 |
    | --------------------------------------- | ----------------------- |
    | `withDelay(ms, animation)`              | Delay before starting   |
    | `withSequence(...animations)`           | Run animations in order |
    | `withRepeat(animation, count, reverse)` | Repeat animation        |
    | `withClamp({ min, max }, animation)`    | Clamp output range      |
    | `cancelAnimation(sharedValue)`          | Stop running animation  |
    
    ### Extrapolation
    
    | Type                     | Behavior                     |
    | ------------------------ | ---------------------------- |
    | `Extrapolation.CLAMP`    | Cap at output range edges    |
    | `Extrapolation.EXTEND`   | Extend linearly beyond range |
    | `Extrapolation.IDENTITY` | Return raw input value       |
    
    ### Layout Animation Modifiers
    
    | Modifier                                     | Purpose                             |
    | -------------------------------------------- | ----------------------------------- |
    | `.duration(ms)`                              | Set animation length                |
    | `.delay(ms)`                                 | Postpone start                      |
    | `.springify()`                               | Use spring physics                  |
    | `.damping(n)` / `.stiffness(n)` / `.mass(n)` | Spring config (after springify)     |
    | `.withInitialValues(style)`                  | Override starting values            |
    | `.withCallback(fn)`                          | Execute on completion               |
    | `.reduceMotion(mode)`                        | Respect accessibility               |
    | `.randomDelay()`                             | Random delay (0 to specified delay) |
    
    ### Animated Components
    
    | Built-in              | Custom                                        |
    | --------------------- | --------------------------------------------- |
    | `Animated.View`       | `Animated.createAnimatedComponent(Component)` |
    | `Animated.Text`       | (call at module level, not in render)         |
    | `Animated.ScrollView` |                                               |
    | `Animated.Image`      |                                               |
    | `Animated.FlatList`   |                                               |
    
  • SKILL.md 18.8 KB
    ---
    name: mobile-animation-reanimated
    description: React Native Reanimated 4 - shared values, animated styles, spring/timing/decay, layout animations, gesture integration, scroll-driven animations, interpolation, worklets, CSS animations
    ---
    
    # React Native Reanimated Patterns
    
    > **Quick Guide:** Reanimated 4 is New Architecture only (requires `react-native-worklets` as a separate dependency). Use `useSharedValue` + `useAnimatedStyle` for all animations. Animations run on the UI thread via worklets -- never block the JS thread. Use `withSpring` (physics-based) or `withTiming` (duration-based) for transitions, layout animations (`entering`/`exiting`) for mount/unmount, and `useScrollOffset` (renamed from `useScrollViewOffset`) for scroll-driven animations. Reanimated 4 also introduces CSS animations/transitions as a declarative alternative to the worklet API.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use Animated components (`Animated.View`, `Animated.Text`, etc.) for any animated styles -- passing animated styles to regular components causes errors)**
    
    **(You MUST keep static styles in `StyleSheet.create` and only animate dynamic properties in `useAnimatedStyle` -- animating static values wastes UI thread resources)**
    
    **(You MUST NOT mutate shared values inside `useAnimatedStyle` callbacks -- read only, or you cause infinite loops)**
    
    **(You MUST use `react-native-worklets` as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin`)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Reanimated, react-native-reanimated, useSharedValue, useAnimatedStyle, withSpring, withTiming, withDecay, Animated.View, Animated.Text, Animated.ScrollView, entering, exiting, FadeIn, FadeOut, SlideIn, interpolate, interpolateColor, useScrollOffset, GestureDetector, Gesture.Pan, worklet, layout animation, shared value, energyThreshold, CSS animation, react-native-worklets
    
    **When to use:**
    
    - Animating view properties (opacity, transforms, colors) on the UI thread
    - Adding entering/exiting animations when components mount/unmount
    - Building gesture-driven animations (drag, swipe, pinch)
    - Creating scroll-driven header collapse, parallax, or sticky effects
    - Interpolating values across ranges (position to opacity, scroll to scale)
    - Implementing spring physics or timing-based transitions
    
    **When NOT to use:**
    
    - Simple boolean show/hide without animation (conditional rendering suffices)
    - Static layouts that never change at runtime
    - Animated.Value from React Native core (use Reanimated's shared values instead)
    
    **Key patterns covered:**
    
    - Shared values (`useSharedValue`) + animated styles (`useAnimatedStyle`)
    - Animation functions: `withTiming`, `withSpring`, `withDecay`
    - Layout animations: `entering`/`exiting` with predefined builders
    - Gesture integration: `Gesture.Pan` + shared values + `withDecay`
    - Scroll-driven animations with `useScrollOffset`
    - Interpolation: `interpolate` and `interpolateColor`
    - Worklet functions and the `'worklet'` directive
    - CSS animations and transitions (Reanimated 4 declarative API)
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Shared values, animated styles, withTiming, withSpring, interpolation
    - [examples/layout-animations.md](examples/layout-animations.md) - Entering/exiting animations, predefined builders, custom layout animations
    - [examples/gestures.md](examples/gestures.md) - Pan gesture with shared values, swipe-to-dismiss, withDecay
    - [examples/scroll-animations.md](examples/scroll-animations.md) - Collapsing header, parallax, scroll-driven opacity
    - [reference.md](reference.md) - Decision frameworks, migration from 3.x, API quick reference
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Reanimated runs animations on the **UI thread** via worklets, keeping the JS thread free for business logic. The core model: **shared values** are reactive state that bridges JS and UI threads, **animated styles** derive visual properties from shared values, and **animation functions** (`withSpring`, `withTiming`, `withDecay`) drive transitions between values.
    
    **Reanimated 4 key changes from 3.x:**
    
    - **New Architecture only** -- drops Legacy Architecture (bridge) support entirely
    - **`react-native-worklets`** as a separate package -- Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin`
    - **CSS animations/transitions** -- declarative API for state-driven animations (use CSS for simple state transitions, worklets for gesture/scroll-driven)
    - **`energyThreshold`** replaces `restDisplacementThreshold`/`restSpeedThreshold` in `withSpring`
    - **`useScrollOffset`** replaces `useScrollViewOffset` (deprecated alias remains)
    - **Threading functions moved** to `react-native-worklets`: `runOnJS` -> `scheduleOnRN`, `runOnUI` -> `scheduleOnUI`
    
    **When to use CSS animations vs worklets:**
    
    - **CSS animations/transitions** -- state-driven, declarative, less code, better optimizable by Reanimated
    - **Worklets** -- gesture-driven, scroll-driven, complex orchestration, frame-by-frame control
    
    **Backward compatibility:** All v2/v3 shared value and animation APIs work unchanged in v4. CSS animations and worklet-based animations work simultaneously and interchangeably.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Shared Values and Animated Styles
    
    The fundamental building blocks. `useSharedValue` creates reactive state on the UI thread. `useAnimatedStyle` derives styles that update when shared values change.
    
    ```typescript
    import Animated, {
      useSharedValue,
      useAnimatedStyle,
      withTiming,
    } from "react-native-reanimated";
    
    const EXPANDED_HEIGHT = 200;
    const COLLAPSED_HEIGHT = 60;
    const ANIMATION_DURATION = 300;
    
    function CollapsibleCard() {
      const height = useSharedValue(COLLAPSED_HEIGHT);
    
      const animatedStyle = useAnimatedStyle(() => ({
        height: height.value,
      }));
    
      const toggle = () => {
        height.value = withTiming(
          height.value === COLLAPSED_HEIGHT ? EXPANDED_HEIGHT : COLLAPSED_HEIGHT,
          { duration: ANIMATION_DURATION }
        );
      };
    
      return (
        <Pressable onPress={toggle}>
          <Animated.View style={[styles.card, animatedStyle]}>
            <Text>Content</Text>
          </Animated.View>
        </Pressable>
      );
    }
    ```
    
    **Why good:** static styles stay in StyleSheet, only dynamic height in useAnimatedStyle, named constants for dimensions and durations, Animated.View receives the animated style
    
    **Key rules:**
    
    - Only animate dynamic properties in `useAnimatedStyle` -- static styles belong in `StyleSheet.create`
    - Never mutate shared values inside `useAnimatedStyle` -- it is read-only
    - Always apply animated styles to `Animated.*` components, not regular `View`/`Text`
    
    See [examples/core.md](examples/core.md) for complete examples including React Compiler compatibility (`get()`/`set()`).
    
    ---
    
    ### Pattern 2: withSpring and withTiming
    
    `withTiming` is duration-based (predictable timing). `withSpring` is physics-based (natural feel). Choose based on UX intent.
    
    ```typescript
    // Physics-based spring (natural, bouncy)
    sv.value = withSpring(TARGET, { damping: 100, stiffness: 800 });
    
    // Duration-based spring (controlled timing with spring feel)
    sv.value = withSpring(TARGET, { duration: 500, dampingRatio: 0.8 });
    
    // Timing with easing
    sv.value = withTiming(TARGET, {
      duration: ANIMATION_DURATION,
      easing: Easing.bezierFn(0.25, 0.1, 0.25, 1),
    });
    ```
    
    **Reanimated 4 spring change:** `restDisplacementThreshold` and `restSpeedThreshold` are removed. Replaced by `energyThreshold` (relative to animation, default `6e-9`). In most cases, removing the old thresholds is sufficient -- no need to set `energyThreshold` manually.
    
    **Duration gotcha:** In v4, actual spring completion time = perceptual `duration` x 1.5. Divide existing duration values by 1.5 for equivalent behavior when migrating from v3.
    
    See [examples/core.md](examples/core.md) for spring config comparison and withDecay.
    
    ---
    
    ### Pattern 3: Layout Animations (Entering/Exiting)
    
    Predefined animations for component mount/unmount. Apply to `Animated.*` components via `entering` and `exiting` props.
    
    ```typescript
    import Animated, { FadeIn, FadeOutLeft } from "react-native-reanimated";
    
    const ANIMATION_DURATION = 400;
    const ANIMATION_DELAY = 100;
    
    function NotificationBanner({ visible }: { visible: boolean }) {
      if (!visible) return null;
    
      return (
        <Animated.View
          entering={FadeIn.duration(ANIMATION_DURATION).delay(ANIMATION_DELAY)}
          exiting={FadeOutLeft.duration(ANIMATION_DURATION)}
          style={styles.banner}
        >
          <Text>New notification</Text>
        </Animated.View>
      );
    }
    ```
    
    **Available builders:** `FadeIn`, `SlideInRight`, `ZoomIn`, `BounceIn`, `FlipInEasyX`, `LightSpeedInRight`, `RotateIn`, `PinwheelIn`, and all directional variants (Up/Down/Left/Right) plus corresponding `Out` variants.
    
    **Modifiers:** `.duration(ms)`, `.delay(ms)`, `.springify()` (with `.damping()`, `.stiffness()`, `.mass()`), `.withInitialValues()`, `.withCallback()`, `.reduceMotion()`.
    
    **Performance tip:** Define animation builders outside components or in `useMemo` -- creating them inline in render causes unnecessary object allocation.
    
    See [examples/layout-animations.md](examples/layout-animations.md) for custom builders, staggered lists, and `EntryExitTransition`.
    
    ---
    
    ### Pattern 4: Gesture Integration
    
    Reanimated integrates with `react-native-gesture-handler`. Gesture callbacks are **automatically workletized** -- you can access shared values directly without the `'worklet'` directive.
    
    ```typescript
    import { Gesture, GestureDetector } from "react-native-gesture-handler";
    import Animated, {
      useSharedValue,
      useAnimatedStyle,
      withDecay,
    } from "react-native-reanimated";
    
    function DraggableCard() {
      const offsetX = useSharedValue(0);
    
      const pan = Gesture.Pan()
        .onChange((e) => {
          offsetX.value += e.changeX;
        })
        .onFinalize((e) => {
          offsetX.value = withDecay({ velocity: e.velocityX, rubberBandEffect: true });
        });
    
      const animatedStyle = useAnimatedStyle(() => ({
        transform: [{ translateX: offsetX.value }],
      }));
    
      return (
        <GestureDetector gesture={pan}>
          <Animated.View style={[styles.card, animatedStyle]} />
        </GestureDetector>
      );
    }
    ```
    
    **Why good:** `onChange` gives delta values (not absolute), `onFinalize` adds momentum with `withDecay`, gesture callbacks access shared values directly on UI thread
    
    **Key points:**
    
    - Use `onChange` for incremental updates (delta), `onUpdate` for absolute position
    - `GestureHandlerRootView` must wrap your app near the root
    - This skill covers the Reanimated side of gesture animations -- for gesture configuration details (tap, pinch, fling, simultaneous gestures), see the gesture handler skill
    
    See [examples/gestures.md](examples/gestures.md) for swipe-to-dismiss, bottom sheet, and combined gestures.
    
    ---
    
    ### Pattern 5: Scroll-Driven Animations
    
    Use `useScrollOffset` to track scroll position as a shared value. Combine with `interpolate` for parallax, collapsing headers, and fade effects.
    
    ```typescript
    import Animated, {
      useAnimatedRef,
      useScrollOffset,
      useAnimatedStyle,
      interpolate,
      Extrapolation,
    } from "react-native-reanimated";
    
    const HEADER_MAX = 200;
    const HEADER_MIN = 60;
    
    function CollapsibleHeader() {
      const scrollRef = useAnimatedRef<Animated.ScrollView>();
      const scrollOffset = useScrollOffset(scrollRef);
    
      const headerStyle = useAnimatedStyle(() => ({
        height: interpolate(
          scrollOffset.value,
          [0, HEADER_MAX - HEADER_MIN],
          [HEADER_MAX, HEADER_MIN],
          Extrapolation.CLAMP
        ),
      }));
    
      return (
        <>
          <Animated.View style={[styles.header, headerStyle]} />
          <Animated.ScrollView ref={scrollRef}>
            {/* content */}
          </Animated.ScrollView>
        </>
      );
    }
    ```
    
    **Why good:** `useScrollOffset` auto-detects horizontal/vertical, no manual scroll event handler needed, `Extrapolation.CLAMP` prevents values outside the range
    
    **Reanimated 4 rename:** `useScrollViewOffset` -> `useScrollOffset` (deprecated alias remains temporarily).
    
    See [examples/scroll-animations.md](examples/scroll-animations.md) for parallax, sticky elements, and scroll-to-hide tab bar.
    
    ---
    
    ### Pattern 6: Interpolation
    
    Map one value range to another. `interpolate` for numbers, `interpolateColor` for color transitions.
    
    ```typescript
    import {
      interpolate,
      interpolateColor,
      Extrapolation,
    } from "react-native-reanimated";
    
    // Number interpolation: scroll position -> opacity
    const opacity = interpolate(
      scrollY.value,
      [0, 100], // input range
      [1, 0], // output range
      Extrapolation.CLAMP,
    );
    
    // Color interpolation: progress -> background
    const backgroundColor = interpolateColor(
      progress.value,
      [0, 0.5, 1], // input range
      ["#FF0000", "#FFFF00", "#00FF00"], // output colors (red -> yellow -> green)
    );
    ```
    
    **Extrapolation options:** `CLAMP` (cap at edges), `EXTEND` (extrapolate linearly), `IDENTITY` (return input value). Can set left/right independently: `{ extrapolateLeft: Extrapolation.CLAMP, extrapolateRight: Extrapolation.EXTEND }`.
    
    **`interpolateColor` modes:** `'RGB'` (default) or `'HSV'`. HSV produces more perceptually uniform transitions for hue changes.
    
    See [examples/core.md](examples/core.md) for multi-step interpolation and color transition examples.
    
    ---
    
    ### Pattern 7: Worklet Functions
    
    Functions that run on the UI thread. Mark with `'worklet'` directive. Reanimated auto-workletizes callbacks in `useAnimatedStyle`, gesture handlers, and animation callbacks -- you only need explicit `'worklet'` for standalone helper functions.
    
    ```typescript
    import { scheduleOnRN } from "react-native-worklets";
    
    function clampValue(value: number, min: number, max: number) {
      "worklet";
      return Math.min(Math.max(value, min), max);
    }
    
    // Use in useAnimatedStyle -- auto-workletized, no directive needed
    const style = useAnimatedStyle(() => ({
      opacity: clampValue(progress.value, 0, 1),
    }));
    ```
    
    **Reanimated 4 threading changes:**
    
    | Reanimated 3         | Reanimated 4 (react-native-worklets) |
    | -------------------- | ------------------------------------ |
    | `runOnJS(fn)("arg")` | `scheduleOnRN(fn, "arg")`            |
    | `runOnUI(fn)("arg")` | `scheduleOnUI(fn, "arg")`            |
    
    **When you need explicit `'worklet'`:**
    
    - Standalone helper functions called from other worklets
    - Functions passed to `scheduleOnUI`
    
    **When you do NOT need it:**
    
    - `useAnimatedStyle` callbacks (auto-workletized)
    - Gesture handler callbacks (auto-workletized)
    - Animation callbacks (auto-workletized)
    
    ---
    
    ### Pattern 8: CSS Animations and Transitions (Reanimated 4)
    
    Declarative animation API modeled after web CSS. Best for state-driven animations. Worklet API remains for gesture/scroll-driven scenarios.
    
    ```typescript
    import Animated from "react-native-reanimated";
    
    const TRANSITION_DURATION = 300;
    
    function ToggleBox({ expanded }: { expanded: boolean }) {
      return (
        <Animated.View
          style={{
            height: expanded ? 200 : 60,
            opacity: expanded ? 1 : 0.5,
            transitionProperty: "height, opacity",
            transitionDuration: `${TRANSITION_DURATION}ms`,
            transitionTimingFunction: "ease-in-out",
          }}
        />
      );
    }
    ```
    
    **CSS animation keyframes:**
    
    ```typescript
    const PULSE_DURATION = 1000;
    
    <Animated.View
      style={{
        animationName: {
          from: { transform: [{ scale: 1 }] },
          to: { transform: [{ scale: 1.1 }] },
        },
        animationDuration: `${PULSE_DURATION}ms`,
        animationIterationCount: "infinite",
        animationDirection: "alternate",
        animationTimingFunction: "ease-in-out",
      }}
    />
    ```
    
    **When to use CSS vs worklets:**
    
    - CSS -- state-driven toggles, hover effects, simple transitions (less code, better optimizable)
    - Worklets -- gesture-driven, scroll-driven, frame-by-frame control, complex orchestration
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Passing animated styles to regular `View`/`Text` instead of `Animated.View`/`Animated.Text` -- causes silent failure or crash
    - Mutating shared values inside `useAnimatedStyle` -- causes infinite re-evaluation loops
    - Using `react-native-reanimated/plugin` in Babel config with Reanimated 4 -- must use `react-native-worklets/plugin` (and it must be last in the plugins array)
    - Using Reanimated 4.x with Legacy Architecture (old bridge) -- Reanimated 4 is New Architecture only
    - Animating static properties in `useAnimatedStyle` instead of keeping them in `StyleSheet` -- wastes UI thread resources
    - Using `restDisplacementThreshold`/`restSpeedThreshold` in `withSpring` -- removed in v4, replaced by `energyThreshold`
    
    **Medium Priority Issues:**
    
    - Creating layout animation builders inline in render -- allocates objects every render; define outside component or in `useMemo`
    - Using `runOnJS`/`runOnUI` instead of `scheduleOnRN`/`scheduleOnUI` -- old API moved to `react-native-worklets`
    - Missing `GestureHandlerRootView` at app root -- gestures silently fail without it
    - Using `useAnimatedGestureHandler` -- removed in v4, migrate to Gesture Handler 2's `Gesture` API
    
    **Gotchas and Edge Cases:**
    
    - `withSpring` duration: actual completion time = perceptual `duration` x 1.5 -- divide v3 duration values by 1.5 when migrating
    - `useScrollOffset` renamed from `useScrollViewOffset` -- deprecated alias still works temporarily
    - Shared value `.value` access is synchronous on UI thread but asynchronous on JS thread -- don't rely on immediate reads after writes on JS thread
    - `useWorkletCallback` removed -- replace with `useCallback` + `'worklet'` directive
    - React Compiler compatibility: use `sv.get()` and `sv.set()` instead of direct `.value` access when using React Compiler
    - `combineTransition` removed -- use `EntryExitTransition.entering(entering).exiting(exiting)`
    - Object shared values: reassign the entire object, never mutate individual properties -- mutations break reactivity tracking
    - Removing an animated style from a view does not unset the animated values -- explicitly set properties to `undefined` to reset
    - On New Architecture, layout animations use `nativeID` internally -- don't overwrite it on animated components
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST use Animated components (`Animated.View`, `Animated.Text`, etc.) for any animated styles -- passing animated styles to regular components causes errors)**
    
    **(You MUST keep static styles in `StyleSheet.create` and only animate dynamic properties in `useAnimatedStyle` -- animating static values wastes UI thread resources)**
    
    **(You MUST NOT mutate shared values inside `useAnimatedStyle` callbacks -- read only, or you cause infinite loops)**
    
    **(You MUST use `react-native-worklets` as a separate dependency in Reanimated 4 -- the worklet Babel plugin moved from `react-native-reanimated/plugin` to `react-native-worklets/plugin`)**
    
    **Failure to follow these rules will cause animation failures, infinite loops, and crashes on the UI thread.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related