From 837e3f4646f471b7d25e96b76f635c4c94764b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ckirukib=E2=80=9D?= <“kirubeljkl679@gmail.com”> Date: Sun, 22 Feb 2026 23:07:15 +0300 Subject: [PATCH] - --- .agents/skills/react-native-design/SKILL.md | 440 +++++++++ .../references/navigation-patterns.md | 832 +++++++++++++++++ .../references/reanimated-patterns.md | 775 ++++++++++++++++ .../references/styling-patterns.md | 871 ++++++++++++++++++ skills-lock.json | 10 + 5 files changed, 2928 insertions(+) create mode 100644 .agents/skills/react-native-design/SKILL.md create mode 100644 .agents/skills/react-native-design/references/navigation-patterns.md create mode 100644 .agents/skills/react-native-design/references/reanimated-patterns.md create mode 100644 .agents/skills/react-native-design/references/styling-patterns.md create mode 100644 skills-lock.json diff --git a/.agents/skills/react-native-design/SKILL.md b/.agents/skills/react-native-design/SKILL.md new file mode 100644 index 0000000..f4b0a82 --- /dev/null +++ b/.agents/skills/react-native-design/SKILL.md @@ -0,0 +1,440 @@ +--- +name: react-native-design +description: Master React Native styling, navigation, and Reanimated animations for cross-platform mobile development. Use when building React Native apps, implementing navigation patterns, or creating performant animations. +--- + +# React Native Design + +Master React Native styling patterns, React Navigation, and Reanimated 3 to build performant, cross-platform mobile applications with native-quality user experiences. + +## When to Use This Skill + +- Building cross-platform mobile apps with React Native +- Implementing navigation with React Navigation 6+ +- Creating performant animations with Reanimated 3 +- Styling components with StyleSheet and styled-components +- Building responsive layouts for different screen sizes +- Implementing platform-specific designs (iOS/Android) +- Creating gesture-driven interactions with Gesture Handler +- Optimizing React Native performance + +## Core Concepts + +### 1. StyleSheet and Styling + +**Basic StyleSheet:** + +```typescript +import { StyleSheet, View, Text } from 'react-native'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#ffffff', + }, + title: { + fontSize: 24, + fontWeight: '600', + color: '#1a1a1a', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#666666', + lineHeight: 24, + }, +}); + +function Card() { + return ( + + Title + Subtitle text + + ); +} +``` + +**Dynamic Styles:** + +```typescript +interface CardProps { + variant: 'primary' | 'secondary'; + disabled?: boolean; +} + +function Card({ variant, disabled }: CardProps) { + return ( + + Content + + ); +} + +const styles = StyleSheet.create({ + card: { + padding: 16, + borderRadius: 12, + }, + primary: { + backgroundColor: '#6366f1', + }, + secondary: { + backgroundColor: '#f3f4f6', + borderWidth: 1, + borderColor: '#e5e7eb', + }, + disabled: { + opacity: 0.5, + }, + text: { + fontSize: 16, + }, +}); +``` + +### 2. Flexbox Layout + +**Row and Column Layouts:** + +```typescript +const styles = StyleSheet.create({ + // Vertical stack (column) + column: { + flexDirection: "column", + gap: 12, + }, + // Horizontal stack (row) + row: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + // Space between items + spaceBetween: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + // Centered content + centered: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + // Fill remaining space + fill: { + flex: 1, + }, +}); +``` + +### 3. React Navigation Setup + +**Stack Navigator:** + +```typescript +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +type RootStackParamList = { + Home: undefined; + Detail: { itemId: string }; + Settings: undefined; +}; + +const Stack = createNativeStackNavigator(); + +function AppNavigator() { + return ( + + + + ({ + title: `Item ${route.params.itemId}`, + })} + /> + + + + ); +} +``` + +**Tab Navigator:** + +```typescript +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { Ionicons } from '@expo/vector-icons'; + +type TabParamList = { + Home: undefined; + Search: undefined; + Profile: undefined; +}; + +const Tab = createBottomTabNavigator(); + +function TabNavigator() { + return ( + ({ + tabBarIcon: ({ focused, color, size }) => { + const icons: Record = { + Home: focused ? 'home' : 'home-outline', + Search: focused ? 'search' : 'search-outline', + Profile: focused ? 'person' : 'person-outline', + }; + return ; + }, + tabBarActiveTintColor: '#6366f1', + tabBarInactiveTintColor: '#9ca3af', + })} + > + + + + + ); +} +``` + +### 4. Reanimated 3 Basics + +**Animated Values:** + +```typescript +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + withTiming, +} from 'react-native-reanimated'; + +function AnimatedBox() { + const scale = useSharedValue(1); + const opacity = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + const handlePress = () => { + scale.value = withSpring(1.2, {}, () => { + scale.value = withSpring(1); + }); + }; + + return ( + + + + ); +} +``` + +**Gesture Handler Integration:** + +```typescript +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, +} from 'react-native-reanimated'; + +function DraggableCard() { + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + + const gesture = Gesture.Pan() + .onUpdate((event) => { + translateX.value = event.translationX; + translateY.value = event.translationY; + }) + .onEnd(() => { + translateX.value = withSpring(0); + translateY.value = withSpring(0); + }); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: translateX.value }, + { translateY: translateY.value }, + ], + })); + + return ( + + + Drag me! + + + ); +} +``` + +### 5. Platform-Specific Styling + +```typescript +import { Platform, StyleSheet } from "react-native"; + +const styles = StyleSheet.create({ + container: { + padding: 16, + ...Platform.select({ + ios: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + android: { + elevation: 4, + }, + }), + }, + text: { + fontFamily: Platform.OS === "ios" ? "SF Pro Text" : "Roboto", + fontSize: 16, + }, +}); + +// Platform-specific components +import { Platform } from "react-native"; +const StatusBarHeight = Platform.OS === "ios" ? 44 : 0; +``` + +## Quick Start Component + +```typescript +import React from 'react'; +import { + View, + Text, + StyleSheet, + Pressable, + Image, +} from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, +} from 'react-native-reanimated'; + +interface ItemCardProps { + title: string; + subtitle: string; + imageUrl: string; + onPress: () => void; +} + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable); + +export function ItemCard({ title, subtitle, imageUrl, onPress }: ItemCardProps) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + return ( + { scale.value = withSpring(0.97); }} + onPressOut={() => { scale.value = withSpring(1); }} + > + + + + {title} + + + {subtitle} + + + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: '#ffffff', + borderRadius: 16, + overflow: 'hidden', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + image: { + width: '100%', + height: 160, + backgroundColor: '#f3f4f6', + }, + content: { + padding: 16, + gap: 4, + }, + title: { + fontSize: 18, + fontWeight: '600', + color: '#1f2937', + }, + subtitle: { + fontSize: 14, + color: '#6b7280', + lineHeight: 20, + }, +}); +``` + +## Best Practices + +1. **Use TypeScript**: Define navigation and prop types for type safety +2. **Memoize Components**: Use `React.memo` and `useCallback` to prevent unnecessary rerenders +3. **Run Animations on UI Thread**: Use Reanimated worklets for 60fps animations +4. **Avoid Inline Styles**: Use StyleSheet.create for performance +5. **Handle Safe Areas**: Use `SafeAreaView` or `useSafeAreaInsets` +6. **Test on Real Devices**: Simulator/emulator performance differs from real devices +7. **Use FlatList for Lists**: Never use ScrollView with map for long lists +8. **Platform-Specific Code**: Use Platform.select for iOS/Android differences + +## Common Issues + +- **Gesture Conflicts**: Wrap gestures with `GestureDetector` and use `simultaneousHandlers` +- **Navigation Type Errors**: Define `ParamList` types for all navigators +- **Animation Jank**: Move animations to UI thread with `runOnUI` worklets +- **Memory Leaks**: Cancel animations and cleanup in useEffect +- **Font Loading**: Use `expo-font` or `react-native-asset` for custom fonts +- **Safe Area Issues**: Test on notched devices (iPhone, Android with cutouts) + +## Resources + +- [React Native Documentation](https://reactnative.dev/) +- [React Navigation](https://reactnavigation.org/) +- [Reanimated Documentation](https://docs.swmansion.com/react-native-reanimated/) +- [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/) +- [Expo Documentation](https://docs.expo.dev/) diff --git a/.agents/skills/react-native-design/references/navigation-patterns.md b/.agents/skills/react-native-design/references/navigation-patterns.md new file mode 100644 index 0000000..bafc27c --- /dev/null +++ b/.agents/skills/react-native-design/references/navigation-patterns.md @@ -0,0 +1,832 @@ +# React Navigation Patterns + +## Setup and Configuration + +### Installation + +```bash +# Core packages +npm install @react-navigation/native +npm install @react-navigation/native-stack +npm install @react-navigation/bottom-tabs + +# Required peer dependencies +npm install react-native-screens react-native-safe-area-context +``` + +### Type-Safe Navigation Setup + +```typescript +// navigation/types.ts +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { BottomTabScreenProps } from "@react-navigation/bottom-tabs"; +import { + CompositeScreenProps, + NavigatorScreenParams, +} from "@react-navigation/native"; + +// Define param lists for each navigator +export type RootStackParamList = { + Main: NavigatorScreenParams; + Modal: { title: string }; + Auth: NavigatorScreenParams; +}; + +export type MainTabParamList = { + Home: undefined; + Search: { query?: string }; + Profile: { userId: string }; +}; + +export type AuthStackParamList = { + Login: undefined; + Register: undefined; + ForgotPassword: { email?: string }; +}; + +// Screen props helpers +export type RootStackScreenProps = + NativeStackScreenProps; + +export type MainTabScreenProps = + CompositeScreenProps< + BottomTabScreenProps, + RootStackScreenProps + >; + +// Global type declaration +declare global { + namespace ReactNavigation { + interface RootParamList extends RootStackParamList {} + } +} +``` + +### Navigation Hooks + +```typescript +// hooks/useAppNavigation.ts +import { useNavigation, useRoute, RouteProp } from "@react-navigation/native"; +import { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import { RootStackParamList } from "./types"; + +export function useAppNavigation() { + return useNavigation>(); +} + +export function useTypedRoute() { + return useRoute>(); +} +``` + +## Stack Navigation + +### Basic Stack Navigator + +```typescript +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { RootStackParamList } from './types'; + +const Stack = createNativeStackNavigator(); + +function RootNavigator() { + return ( + + + + + + + + ); +} +``` + +### Screen with Dynamic Options + +```typescript +function DetailScreen({ route, navigation }: DetailScreenProps) { + const { itemId } = route.params; + const [item, setItem] = useState(null); + + useEffect(() => { + // Update header when data loads + if (item) { + navigation.setOptions({ + title: item.title, + headerRight: () => ( + shareItem(item)}> + + + ), + }); + } + }, [item, navigation]); + + // Prevent going back with unsaved changes + useEffect(() => { + const unsubscribe = navigation.addListener('beforeRemove', (e) => { + if (!hasUnsavedChanges) return; + + e.preventDefault(); + Alert.alert( + 'Discard changes?', + 'You have unsaved changes. Are you sure you want to leave?', + [ + { text: "Don't leave", style: 'cancel' }, + { + text: 'Discard', + style: 'destructive', + onPress: () => navigation.dispatch(e.data.action), + }, + ] + ); + }); + + return unsubscribe; + }, [navigation, hasUnsavedChanges]); + + return {/* Content */}; +} +``` + +## Tab Navigation + +### Bottom Tab Navigator + +```typescript +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { MainTabParamList } from './types'; +import { Ionicons } from '@expo/vector-icons'; + +const Tab = createBottomTabNavigator(); + +function MainTabNavigator() { + return ( + ({ + tabBarIcon: ({ focused, color, size }) => { + const icons: Record = { + Home: focused ? 'home' : 'home-outline', + Search: focused ? 'search' : 'search-outline', + Profile: focused ? 'person' : 'person-outline', + }; + return ( + + ); + }, + tabBarActiveTintColor: '#6366f1', + tabBarInactiveTintColor: '#9ca3af', + tabBarStyle: { + backgroundColor: '#ffffff', + borderTopWidth: 1, + borderTopColor: '#e5e7eb', + paddingBottom: 8, + paddingTop: 8, + height: 60, + }, + tabBarLabelStyle: { + fontSize: 12, + fontWeight: '500', + }, + headerStyle: { backgroundColor: '#ffffff' }, + headerTitleStyle: { fontWeight: '600' }, + })} + > + + + + + ); +} +``` + +### Custom Tab Bar + +```typescript +import { View, Pressable, StyleSheet } from 'react-native'; +import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, +} from 'react-native-reanimated'; + +function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) { + return ( + + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]; + const label = options.tabBarLabel ?? route.name; + const isFocused = state.index === index; + + const onPress = () => { + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name); + } + }; + + return ( + + ); + })} + + ); +} + +function TabBarButton({ label, isFocused, onPress, icon }) { + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + return ( + { scale.value = withSpring(0.9); }} + onPressOut={() => { scale.value = withSpring(1); }} + style={styles.tabButton} + > + + {icon?.({ + focused: isFocused, + color: isFocused ? '#6366f1' : '#9ca3af', + size: 24, + })} + + {label} + + + + ); +} + +const styles = StyleSheet.create({ + tabBar: { + flexDirection: 'row', + backgroundColor: '#ffffff', + paddingBottom: 20, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: '#e5e7eb', + }, + tabButton: { + flex: 1, + alignItems: 'center', + }, + tabLabel: { + fontSize: 12, + marginTop: 4, + fontWeight: '500', + }, +}); + +// Usage + }> + {/* screens */} + +``` + +## Drawer Navigation + +```typescript +import { + createDrawerNavigator, + DrawerContentScrollView, + DrawerItemList, + DrawerContentComponentProps, +} from '@react-navigation/drawer'; + +const Drawer = createDrawerNavigator(); + +function CustomDrawerContent(props: DrawerContentComponentProps) { + return ( + + + + {user.name} + {user.email} + + + + + + Log Out + + + + ); +} + +function DrawerNavigator() { + return ( + } + screenOptions={{ + drawerActiveBackgroundColor: '#ede9fe', + drawerActiveTintColor: '#6366f1', + drawerInactiveTintColor: '#4b5563', + drawerLabelStyle: { marginLeft: -20, fontSize: 15, fontWeight: '500' }, + drawerStyle: { width: 280 }, + }} + > + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} +``` + +## Deep Linking + +### Configuration + +```typescript +// navigation/linking.ts +import { LinkingOptions } from '@react-navigation/native'; +import { RootStackParamList } from './types'; + +export const linking: LinkingOptions = { + prefixes: ['myapp://', 'https://myapp.com'], + config: { + screens: { + Main: { + screens: { + Home: 'home', + Search: 'search', + Profile: 'profile/:userId', + }, + }, + Modal: 'modal/:title', + Auth: { + screens: { + Login: 'login', + Register: 'register', + ForgotPassword: 'forgot-password', + }, + }, + }, + }, + // Custom URL parsing + getStateFromPath: (path, config) => { + // Handle custom URL patterns + return getStateFromPath(path, config); + }, +}; + +// App.tsx +function App() { + return ( + }> + + + ); +} +``` + +### Handling Deep Links + +```typescript +import { useEffect } from "react"; +import { Linking } from "react-native"; +import { useNavigation } from "@react-navigation/native"; + +function useDeepLinkHandler() { + const navigation = useNavigation(); + + useEffect(() => { + // Handle initial URL + const handleInitialUrl = async () => { + const url = await Linking.getInitialURL(); + if (url) { + handleDeepLink(url); + } + }; + + // Handle URL changes + const subscription = Linking.addEventListener("url", ({ url }) => { + handleDeepLink(url); + }); + + handleInitialUrl(); + + return () => subscription.remove(); + }, []); + + const handleDeepLink = (url: string) => { + // Parse URL and navigate + const route = parseUrl(url); + if (route) { + navigation.navigate(route.name, route.params); + } + }; +} +``` + +## Navigation State Management + +### Auth Flow + +```typescript +import { createContext, useContext, useState, useEffect } from 'react'; + +interface AuthContextType { + user: User | null; + isLoading: boolean; + signIn: (email: string, password: string) => Promise; + signOut: () => Promise; +} + +const AuthContext = createContext(null!); + +function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Check for existing session + checkAuthState(); + }, []); + + const checkAuthState = async () => { + try { + const token = await AsyncStorage.getItem('token'); + if (token) { + const user = await fetchUser(token); + setUser(user); + } + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const signIn = async (email: string, password: string) => { + const { user, token } = await loginApi(email, password); + await AsyncStorage.setItem('token', token); + setUser(user); + }; + + const signOut = async () => { + await AsyncStorage.removeItem('token'); + setUser(null); + }; + + return ( + + {children} + + ); +} + +function RootNavigator() { + const { user, isLoading } = useAuth(); + + if (isLoading) { + return ; + } + + return ( + + {user ? ( + + ) : ( + + )} + + ); +} +``` + +### Navigation State Persistence + +```typescript +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { NavigationContainer, NavigationState } from '@react-navigation/native'; + +const PERSISTENCE_KEY = 'NAVIGATION_STATE'; + +function App() { + const [isReady, setIsReady] = useState(false); + const [initialState, setInitialState] = useState(); + + useEffect(() => { + const restoreState = async () => { + try { + const savedState = await AsyncStorage.getItem(PERSISTENCE_KEY); + if (savedState) { + setInitialState(JSON.parse(savedState)); + } + } catch (e) { + console.error('Failed to restore navigation state:', e); + } finally { + setIsReady(true); + } + }; + + if (!isReady) { + restoreState(); + } + }, [isReady]); + + if (!isReady) { + return ; + } + + return ( + { + AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)); + }} + > + + + ); +} +``` + +## Screen Transitions + +### Custom Animations + +```typescript +import { TransitionPresets } from '@react-navigation/native-stack'; + + + {/* Standard slide transition */} + + + {/* Modal with custom animation */} + + + {/* Full screen modal */} + + +``` + +### Shared Element Transitions + +```typescript +import { SharedElement } from 'react-navigation-shared-element'; +import { createSharedElementStackNavigator } from 'react-navigation-shared-element'; + +const Stack = createSharedElementStackNavigator(); + +function ListScreen({ navigation }) { + return ( + ( + navigation.navigate('Detail', { item })}> + + + + + {item.title} + + + )} + /> + ); +} + +function DetailScreen({ route }) { + const { item } = route.params; + + return ( + + + + + + {item.title} + + + ); +} + +// Navigator configuration + + + { + const { item } = route.params; + return [ + { id: `item.${item.id}.photo`, animation: 'move' }, + { id: `item.${item.id}.title`, animation: 'fade' }, + ]; + }} + /> + +``` + +## Header Customization + +### Custom Header Component + +```typescript +import { getHeaderTitle } from '@react-navigation/elements'; +import { NativeStackHeaderProps } from '@react-navigation/native-stack'; + +function CustomHeader({ navigation, route, options, back }: NativeStackHeaderProps) { + const title = getHeaderTitle(options, route.name); + + return ( + + {back && ( + + + + )} + {title} + {options.headerRight && ( + + {options.headerRight({ canGoBack: !!back })} + + )} + + ); +} + +// Usage + , + }} +> + {/* screens */} + +``` + +### Collapsible Header + +```typescript +import Animated, { + useSharedValue, + useAnimatedScrollHandler, + useAnimatedStyle, + interpolate, + Extrapolation, +} from 'react-native-reanimated'; + +const HEADER_HEIGHT = 200; +const COLLAPSED_HEIGHT = 60; + +function CollapsibleHeaderScreen() { + const scrollY = useSharedValue(0); + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y; + }, + }); + + const headerStyle = useAnimatedStyle(() => { + const height = interpolate( + scrollY.value, + [0, HEADER_HEIGHT - COLLAPSED_HEIGHT], + [HEADER_HEIGHT, COLLAPSED_HEIGHT], + Extrapolation.CLAMP + ); + + return { height }; + }); + + const titleStyle = useAnimatedStyle(() => { + const fontSize = interpolate( + scrollY.value, + [0, HEADER_HEIGHT - COLLAPSED_HEIGHT], + [32, 18], + Extrapolation.CLAMP + ); + + return { fontSize }; + }); + + return ( + + + + Title + + + + + {/* Content */} + + + ); +} +``` diff --git a/.agents/skills/react-native-design/references/reanimated-patterns.md b/.agents/skills/react-native-design/references/reanimated-patterns.md new file mode 100644 index 0000000..7af151d --- /dev/null +++ b/.agents/skills/react-native-design/references/reanimated-patterns.md @@ -0,0 +1,775 @@ +# React Native Reanimated 3 Patterns + +## Core Concepts + +### Shared Values and Animated Styles + +```typescript +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + withTiming, + withDelay, + withSequence, + withRepeat, + Easing, +} from 'react-native-reanimated'; + +function BasicAnimations() { + // Shared value - can be modified from JS or UI thread + const opacity = useSharedValue(0); + const scale = useSharedValue(1); + const rotation = useSharedValue(0); + + // Animated style - runs on UI thread + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [ + { scale: scale.value }, + { rotate: `${rotation.value}deg` }, + ], + })); + + const animate = () => { + // Spring animation + scale.value = withSpring(1.2, { + damping: 10, + stiffness: 100, + }); + + // Timing animation with easing + opacity.value = withTiming(1, { + duration: 500, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + + // Sequence of animations + rotation.value = withSequence( + withTiming(15, { duration: 100 }), + withTiming(-15, { duration: 100 }), + withTiming(0, { duration: 100 }) + ); + }; + + return ( + + ); +} +``` + +### Animation Callbacks + +```typescript +import { runOnJS, runOnUI } from 'react-native-reanimated'; + +function AnimationWithCallbacks() { + const translateX = useSharedValue(0); + const [status, setStatus] = useState('idle'); + + const updateStatus = (newStatus: string) => { + setStatus(newStatus); + }; + + const animate = () => { + translateX.value = withTiming( + 200, + { duration: 1000 }, + (finished) => { + 'worklet'; + if (finished) { + // Call JS function from worklet + runOnJS(updateStatus)('completed'); + } + } + ); + }; + + return ( + ({ + transform: [{ translateX: translateX.value }], + })), + ]} + /> + ); +} +``` + +## Gesture Handler Integration + +### Pan Gesture + +```typescript +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + clamp, +} from 'react-native-reanimated'; + +function DraggableBox() { + const translateX = useSharedValue(0); + const translateY = useSharedValue(0); + const context = useSharedValue({ x: 0, y: 0 }); + + const panGesture = Gesture.Pan() + .onStart(() => { + context.value = { x: translateX.value, y: translateY.value }; + }) + .onUpdate((event) => { + translateX.value = event.translationX + context.value.x; + translateY.value = event.translationY + context.value.y; + }) + .onEnd((event) => { + // Apply velocity decay + translateX.value = withSpring( + clamp(translateX.value + event.velocityX / 10, -100, 100) + ); + translateY.value = withSpring( + clamp(translateY.value + event.velocityY / 10, -100, 100) + ); + }); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: translateX.value }, + { translateY: translateY.value }, + ], + })); + + return ( + + + + ); +} +``` + +### Pinch and Rotate Gestures + +```typescript +function ZoomableImage() { + const scale = useSharedValue(1); + const rotation = useSharedValue(0); + const savedScale = useSharedValue(1); + const savedRotation = useSharedValue(0); + + const pinchGesture = Gesture.Pinch() + .onUpdate((event) => { + scale.value = savedScale.value * event.scale; + }) + .onEnd(() => { + savedScale.value = scale.value; + // Snap back if too small + if (scale.value < 1) { + scale.value = withSpring(1); + savedScale.value = 1; + } + }); + + const rotateGesture = Gesture.Rotation() + .onUpdate((event) => { + rotation.value = savedRotation.value + event.rotation; + }) + .onEnd(() => { + savedRotation.value = rotation.value; + }); + + const composed = Gesture.Simultaneous(pinchGesture, rotateGesture); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { scale: scale.value }, + { rotate: `${rotation.value}rad` }, + ], + })); + + return ( + + + + ); +} +``` + +### Tap Gesture with Feedback + +```typescript +function TappableCard({ onPress, children }: TappableCardProps) { + const scale = useSharedValue(1); + const opacity = useSharedValue(1); + + const tapGesture = Gesture.Tap() + .onBegin(() => { + scale.value = withSpring(0.97); + opacity.value = withTiming(0.8, { duration: 100 }); + }) + .onFinalize(() => { + scale.value = withSpring(1); + opacity.value = withTiming(1, { duration: 100 }); + }) + .onEnd(() => { + runOnJS(onPress)(); + }); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + opacity: opacity.value, + })); + + return ( + + + {children} + + + ); +} +``` + +## Common Animation Patterns + +### Fade In/Out + +```typescript +function FadeInView({ visible, children }: FadeInViewProps) { + const opacity = useSharedValue(0); + + useEffect(() => { + opacity.value = withTiming(visible ? 1 : 0, { duration: 300 }); + }, [visible]); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + display: opacity.value === 0 ? 'none' : 'flex', + })); + + return ( + + {children} + + ); +} +``` + +### Slide In/Out + +```typescript +function SlideInView({ visible, direction = 'right', children }) { + const translateX = useSharedValue(direction === 'right' ? 100 : -100); + const opacity = useSharedValue(0); + + useEffect(() => { + if (visible) { + translateX.value = withSpring(0); + opacity.value = withTiming(1); + } else { + translateX.value = withSpring(direction === 'right' ? 100 : -100); + opacity.value = withTiming(0); + } + }, [visible, direction]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: translateX.value }], + opacity: opacity.value, + })); + + return ( + + {children} + + ); +} +``` + +### Staggered List Animation + +```typescript +function StaggeredList({ items }: { items: Item[] }) { + return ( + item.id} + renderItem={({ item, index }) => ( + + )} + /> + ); +} + +function StaggeredItem({ item, index }: { item: Item; index: number }) { + const translateY = useSharedValue(50); + const opacity = useSharedValue(0); + + useEffect(() => { + translateY.value = withDelay( + index * 100, + withSpring(0, { damping: 15 }) + ); + opacity.value = withDelay( + index * 100, + withTiming(1, { duration: 300 }) + ); + }, [index]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], + opacity: opacity.value, + })); + + return ( + + {item.title} + + ); +} +``` + +### Pulse Animation + +```typescript +function PulseView({ children }: { children: React.ReactNode }) { + const scale = useSharedValue(1); + + useEffect(() => { + scale.value = withRepeat( + withSequence( + withTiming(1.05, { duration: 500 }), + withTiming(1, { duration: 500 }) + ), + -1, // infinite + false // no reverse + ); + + return () => { + cancelAnimation(scale); + }; + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + + return ( + + {children} + + ); +} +``` + +### Shake Animation + +```typescript +function ShakeView({ trigger, children }) { + const translateX = useSharedValue(0); + + useEffect(() => { + if (trigger) { + translateX.value = withSequence( + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(-10, { duration: 50 }), + withTiming(10, { duration: 50 }), + withTiming(0, { duration: 50 }) + ); + } + }, [trigger]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: translateX.value }], + })); + + return ( + + {children} + + ); +} +``` + +## Advanced Patterns + +### Interpolation + +```typescript +import { interpolate, Extrapolation } from 'react-native-reanimated'; + +function ParallaxHeader() { + const scrollY = useSharedValue(0); + + const headerStyle = useAnimatedStyle(() => { + const height = interpolate( + scrollY.value, + [0, 200], + [300, 100], + Extrapolation.CLAMP + ); + + const opacity = interpolate( + scrollY.value, + [0, 150, 200], + [1, 0.5, 0], + Extrapolation.CLAMP + ); + + const translateY = interpolate( + scrollY.value, + [0, 200], + [0, -50], + Extrapolation.CLAMP + ); + + return { + height, + opacity, + transform: [{ translateY }], + }; + }); + + return ( + + + Header + + { + scrollY.value = event.contentOffset.y; + }, + })} + scrollEventThrottle={16} + > + {/* Content */} + + + ); +} +``` + +### Color Interpolation + +```typescript +import { interpolateColor } from 'react-native-reanimated'; + +function ColorTransition() { + const progress = useSharedValue(0); + + const animatedStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor( + progress.value, + [0, 0.5, 1], + ['#6366f1', '#8b5cf6', '#ec4899'] + ); + + return { backgroundColor }; + }); + + const toggleColor = () => { + progress.value = withTiming(progress.value === 0 ? 1 : 0, { + duration: 1000, + }); + }; + + return ( + + + + ); +} +``` + +### Derived Values + +```typescript +import { useDerivedValue } from 'react-native-reanimated'; + +function DerivedValueExample() { + const x = useSharedValue(0); + const y = useSharedValue(0); + + // Derived value computed from other shared values + const distance = useDerivedValue(() => { + return Math.sqrt(x.value ** 2 + y.value ** 2); + }); + + const angle = useDerivedValue(() => { + return Math.atan2(y.value, x.value) * (180 / Math.PI); + }); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: x.value }, + { translateY: y.value }, + { rotate: `${angle.value}deg` }, + ], + opacity: interpolate(distance.value, [0, 200], [1, 0.5]), + })); + + return ( + + ); +} +``` + +### Layout Animations + +```typescript +import Animated, { + Layout, + FadeIn, + FadeOut, + SlideInLeft, + SlideOutRight, + ZoomIn, + ZoomOut, +} from 'react-native-reanimated'; + +function AnimatedList() { + const [items, setItems] = useState([1, 2, 3, 4, 5]); + + const addItem = () => { + setItems([...items, items.length + 1]); + }; + + const removeItem = (id: number) => { + setItems(items.filter((item) => item !== id)); + }; + + return ( + +