diff --git a/.github/skills/react-native-skills/AGENTS.md b/.github/skills/react-native-skills/AGENTS.md
new file mode 100644
index 0000000..c3f43cf
--- /dev/null
+++ b/.github/skills/react-native-skills/AGENTS.md
@@ -0,0 +1,1051 @@
+# React Native & Expo Skills
+
+**Version 2.0.0**
+BitSleuth
+January 2026
+
+> **Note:**
+> This document is for agents and LLMs to follow when maintaining,
+> generating, or refactoring React Native codebases. Humans
+> may also find it useful, but guidance here is optimized for automation
+> and consistency by AI-assisted workflows.
+
+---
+
+## Abstract
+
+Performance and best practices guide for React Native and Expo mobile applications targeting iOS and Android. Contains 25+ rules across 11 categories covering core rendering, list performance, animations with Reanimated, navigation with Expo Router, styling with NativeWind, and platform-specific optimizations. Each rule includes detailed explanations, incorrect vs. correct code examples, and impact assessments.
+
+---
+
+## Table of Contents
+
+1. [Core Rendering](#1-core-rendering) — **CRITICAL**
+ - 1.1 [Never Use && with Potentially Falsy Values](#11-never-use--with-potentially-falsy-values)
+ - 1.2 [Wrap Strings in Text Components](#12-wrap-strings-in-text-components)
+2. [List Performance](#2-list-performance) — **HIGH**
+ - 2.1 [Use a List Virtualizer for Any List](#21-use-a-list-virtualizer-for-any-list)
+ - 2.2 [Optimize List Performance with Stable Object References](#22-optimize-list-performance-with-stable-object-references)
+ - 2.3 [Avoid Inline Objects in renderItem](#23-avoid-inline-objects-in-renderitem)
+ - 2.4 [Hoist Callbacks to the Root of Lists](#24-hoist-callbacks-to-the-root-of-lists)
+ - 2.5 [Pass Primitives to List Items for Memoization](#25-pass-primitives-to-list-items-for-memoization)
+ - 2.6 [Keep List Items Lightweight](#26-keep-list-items-lightweight)
+ - 2.7 [Use Compressed Images in Lists](#27-use-compressed-images-in-lists)
+ - 2.8 [Use Item Types for Heterogeneous Lists](#28-use-item-types-for-heterogeneous-lists)
+3. [Animation](#3-animation) — **HIGH**
+ - 3.1 [Animate Transform and Opacity Instead of Layout Properties](#31-animate-transform-and-opacity-instead-of-layout-properties)
+ - 3.2 [Prefer useDerivedValue Over useAnimatedReaction](#32-prefer-usederivedvalue-over-useanimatedreaction)
+ - 3.3 [Use GestureDetector for Animated Press States](#33-use-gesturedetector-for-animated-press-states)
+4. [Scroll Performance](#4-scroll-performance) — **HIGH**
+ - 4.1 [Never Track Scroll Position in useState](#41-never-track-scroll-position-in-usestate)
+5. [Navigation](#5-navigation) — **HIGH**
+ - 5.1 [Use Expo Router for File-Based Navigation](#51-use-expo-router-for-file-based-navigation)
+6. [React State](#6-react-state) — **MEDIUM**
+ - 6.1 [Minimize State Variables and Derive Values](#61-minimize-state-variables-and-derive-values)
+ - 6.2 [Use Fallback State Instead of initialState](#62-use-fallback-state-instead-of-initialstate)
+ - 6.3 [Use Dispatch Updaters for State That Depends on Current Value](#63-use-dispatch-updaters-for-state-that-depends-on-current-value)
+7. [State Architecture](#7-state-architecture) — **MEDIUM**
+ - 7.1 [State Must Represent Ground Truth](#71-state-must-represent-ground-truth)
+8. [React Compiler](#8-react-compiler) — **MEDIUM**
+ - 8.1 [Destructure Functions Early in Render](#81-destructure-functions-early-in-render)
+ - 8.2 [Use .get() and .set() for Reanimated Shared Values](#82-use-get-and-set-for-reanimated-shared-values)
+9. [User Interface](#9-user-interface) — **MEDIUM**
+ - 9.1 [Use expo-image for Optimized Images](#91-use-expo-image-for-optimized-images)
+ - 9.2 [Styling with NativeWind and Modern Patterns](#92-styling-with-nativewind-and-modern-patterns)
+ - 9.3 [Use Pressable Instead of Touchable Components](#93-use-pressable-instead-of-touchable-components)
+ - 9.4 [Use contentInsetAdjustmentBehavior for Safe Areas](#94-use-contentinsetadjustmentbehavior-for-safe-areas)
+ - 9.5 [Use contentInset for Dynamic ScrollView Spacing](#95-use-contentinset-for-dynamic-scrollview-spacing)
+ - 9.6 [Use Native Menus for Dropdowns and Context Menus](#96-use-native-menus-for-dropdowns-and-context-menus)
+ - 9.7 [Use Native Modals Over JS-Based Bottom Sheets](#97-use-native-modals-over-js-based-bottom-sheets)
+ - 9.8 [Measuring View Dimensions](#98-measuring-view-dimensions)
+10. [JavaScript](#10-javascript) — **LOW**
+ - 10.1 [Hoist Intl Formatter Creation](#101-hoist-intl-formatter-creation)
+11. [Fonts](#11-fonts) — **LOW**
+ - 11.1 [Load Fonts Natively at Build Time](#111-load-fonts-natively-at-build-time)
+
+---
+
+## 1. Core Rendering
+
+**Impact: CRITICAL**
+
+Fundamental React Native rendering rules. Violations cause runtime crashes or broken UI.
+
+### 1.1 Never Use && with Potentially Falsy Values
+
+**Impact: CRITICAL (prevents production crash)**
+
+Never use `{value && }` when `value` could be an empty string or `0`. These are falsy but JSX-renderable—React Native will try to render them as text outside a `` component, causing a hard crash in production.
+
+**Incorrect (crashes if count is 0 or name is ""):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {name && {name}}
+ {count && {count} items}
+
+ )
+}
+// If name="" or count=0, renders the falsy value → crash
+```
+
+**Correct (ternary with null):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {name ? {name} : null}
+ {count ? {count} items : null}
+
+ )
+}
+```
+
+**Correct (explicit boolean coercion):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {!!name && {name}}
+ {!!count && {count} items}
+
+ )
+}
+```
+
+**Best (early return):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ if (!name) return null
+
+ return (
+
+ {name}
+ {count > 0 ? {count} items : null}
+
+ )
+}
+```
+
+**Lint rule:** Enable `react/jsx-no-leaked-render` from [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md).
+
+### 1.2 Wrap Strings in Text Components
+
+**Impact: CRITICAL (prevents runtime crash)**
+
+Strings must be rendered inside ``. React Native crashes if a string is a direct child of ``.
+
+**Incorrect (crashes):**
+
+```tsx
+import { View } from 'react-native'
+
+function Greeting({ name }: { name: string }) {
+ return Hello, {name}!
+}
+// Error: Text strings must be rendered within a component.
+```
+
+**Correct:**
+
+```tsx
+import { View, Text } from 'react-native'
+
+function Greeting({ name }: { name: string }) {
+ return (
+
+ Hello, {name}!
+
+ )
+}
+```
+
+---
+
+## 2. List Performance
+
+**Impact: HIGH**
+
+Optimizing virtualized lists (FlatList, FlashList) for smooth scrolling and fast updates.
+
+### 2.1 Use a List Virtualizer for Any List
+
+**Impact: HIGH (reduced memory, faster mounts)**
+
+Use a list virtualizer like FlashList instead of ScrollView with mapped children—even for short lists. Virtualizers only render visible items, reducing memory usage and mount time.
+
+**Incorrect (ScrollView renders all items at once):**
+
+```tsx
+function Feed({ items }: { items: Item[] }) {
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ )
+}
+// 50 items = 50 components mounted, even if only 10 visible
+```
+
+**Correct (virtualizer renders only visible items):**
+
+```tsx
+import { FlashList } from '@shopify/flash-list'
+
+function Feed({ items }: { items: Item[] }) {
+ return (
+ }
+ keyExtractor={(item) => item.id}
+ estimatedItemSize={80}
+ />
+ )
+}
+// Only ~10-15 visible items mounted at a time
+```
+
+### 2.2 Optimize List Performance with Stable Object References
+
+**Impact: CRITICAL (virtualization relies on reference stability)**
+
+Don't map or filter data before passing to virtualized lists. Virtualization relies on object reference stability to know what changed—new references cause full re-renders of all visible items.
+
+**Incorrect (creates new object references on every keystroke):**
+
+```tsx
+function DomainSearch() {
+ const { keyword, setKeyword } = useKeywordState()
+ const { data: tlds } = useTlds()
+
+ // Bad: creates new objects on every render
+ const domains = tlds.map((tld) => ({
+ domain: `${keyword}.${tld.name}`,
+ tld: tld.name,
+ }))
+
+ return (
+ <>
+
+ } />
+ >
+ )
+}
+```
+
+**Correct (stable references, transform inside items):**
+
+```tsx
+function DomainSearch() {
+ const { data: tlds } = useTlds()
+
+ return (
+ }
+ />
+ )
+}
+
+function DomainItem({ tld }: { tld: Tld }) {
+ // Transform within items using Zustand selector
+ const keyword = useSearchStore((s) => s.keyword)
+ const domain = `${keyword}.${tld.name}`
+ return {domain}
+}
+```
+
+### 2.3 Avoid Inline Objects in renderItem
+
+**Impact: HIGH (prevents unnecessary re-renders of memoized list items)**
+
+Don't create new objects inside `renderItem` to pass as props. Inline objects create new references on every render, breaking memoization.
+
+**Incorrect (inline object breaks memoization):**
+
+```tsx
+renderItem={({ item }) => (
+
+)}
+```
+
+**Correct (pass item directly or primitives):**
+
+```tsx
+renderItem={({ item }) => (
+
+)}
+
+const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
+ const backgroundColor = isActive ? 'green' : 'gray'
+ return {/* ... */}
+})
+```
+
+### 2.4 Hoist Callbacks to the Root of Lists
+
+**Impact: MEDIUM (fewer re-renders and faster lists)**
+
+When passing callback functions to list items, create a single instance of the callback at the root of the list.
+
+**Incorrect (creates a new callback on each render):**
+
+```tsx
+renderItem={({ item }) => {
+ const onPress = () => handlePress(item.id) // Bad: new callback each render
+ return
+}}
+```
+
+**Correct (single function instance):**
+
+```tsx
+const onPress = useCallback((id: string) => handlePress(id), [handlePress])
+
+renderItem={({ item }) => (
+
+)}
+```
+
+### 2.5 Pass Primitives to List Items for Memoization
+
+**Impact: HIGH (enables effective memo() comparison)**
+
+Pass only primitive values (strings, numbers, booleans) as props to list item components. Primitives enable shallow comparison in `memo()` to work correctly.
+
+**Incorrect (object prop requires deep comparison):**
+
+```tsx
+const UserRow = memo(function UserRow({ user }: { user: User }) {
+ return {user.name}
+})
+
+renderItem={({ item }) => }
+```
+
+**Correct (primitive props):**
+
+```tsx
+const UserRow = memo(function UserRow({ id, name, email }: Props) {
+ return {name}
+})
+
+renderItem={({ item }) => (
+
+)}
+```
+
+### 2.6 Keep List Items Lightweight
+
+**Impact: HIGH (reduces render time for visible items during scroll)**
+
+List items should be as inexpensive as possible to render. Minimize hooks, avoid queries, and limit React Context access.
+
+**Incorrect (heavy list item):**
+
+```tsx
+function ProductRow({ id }: { id: string }) {
+ const { data: product } = useQuery(['product', id], () => fetchProduct(id))
+ const theme = useContext(ThemeContext)
+ const cart = useContext(CartContext)
+ return {/* ... */}
+}
+```
+
+**Correct (lightweight list item):**
+
+```tsx
+function ProductRow({ name, price, imageUrl }: Props) {
+ return (
+
+
+ {name}
+ {price}
+
+ )
+}
+```
+
+**Guidelines:**
+- No queries or data fetching inside items
+- Prefer Zustand selectors over React Context
+- Minimize useState/useEffect hooks
+- Pass pre-computed values as props
+
+### 2.7 Use Compressed Images in Lists
+
+**Impact: HIGH (faster load times, less memory)**
+
+Always load compressed, appropriately-sized images in lists. Request images at 2x the display size for retina screens.
+
+**Incorrect:**
+
+```tsx
+
+// 4000x3000 image loaded for a 100x100 thumbnail
+```
+
+**Correct:**
+
+```tsx
+const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
+
+```
+
+### 2.8 Use Item Types for Heterogeneous Lists
+
+**Impact: HIGH (efficient recycling, less layout thrashing)**
+
+When a list has different item layouts, use a `type` field and provide `getItemType` to the list.
+
+**Correct (typed items with separate components):**
+
+```tsx
+type FeedItem = HeaderItem | MessageItem | ImageItem
+
+function Feed({ items }: { items: FeedItem[] }) {
+ return (
+ item.id}
+ getItemType={(item) => item.type}
+ renderItem={({ item }) => {
+ switch (item.type) {
+ case 'header': return
+ case 'message': return
+ case 'image': return
+ }
+ }}
+ />
+ )
+}
+```
+
+---
+
+## 3. Animation
+
+**Impact: HIGH**
+
+GPU-accelerated animations, Reanimated patterns, and avoiding render thrashing during gestures.
+
+### 3.1 Animate Transform and Opacity Instead of Layout Properties
+
+**Impact: HIGH (GPU-accelerated animations, no layout recalculation)**
+
+Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. Use `transform` and `opacity` which run on the GPU.
+
+**Incorrect (animates height, triggers layout every frame):**
+
+```tsx
+const animatedStyle = useAnimatedStyle(() => ({
+ height: withTiming(expanded ? 200 : 0),
+}))
+```
+
+**Correct (animates scaleY, GPU-accelerated):**
+
+```tsx
+const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scaleY: withTiming(expanded ? 1 : 0) }],
+ opacity: withTiming(expanded ? 1 : 0),
+}))
+```
+
+### 3.2 Prefer useDerivedValue Over useAnimatedReaction
+
+**Impact: MEDIUM (cleaner code, automatic dependency tracking)**
+
+When deriving a shared value from another, use `useDerivedValue` instead of `useAnimatedReaction`.
+
+**Incorrect:**
+
+```tsx
+const progress = useSharedValue(0)
+const opacity = useSharedValue(1)
+
+useAnimatedReaction(
+ () => progress.value,
+ (current) => { opacity.value = 1 - current }
+)
+```
+
+**Correct:**
+
+```tsx
+const progress = useSharedValue(0)
+const opacity = useDerivedValue(() => 1 - progress.get())
+```
+
+### 3.3 Use GestureDetector for Animated Press States
+
+**Impact: MEDIUM (UI thread animations, smoother press feedback)**
+
+For animated press states, use `GestureDetector` with `Gesture.Tap()` and shared values instead of Pressable's callbacks.
+
+**Correct (GestureDetector with UI thread worklets):**
+
+```tsx
+import { Gesture, GestureDetector } from 'react-native-gesture-handler'
+import Animated, { useSharedValue, useAnimatedStyle, withTiming, interpolate, runOnJS } from 'react-native-reanimated'
+
+function AnimatedButton({ onPress }: { onPress: () => void }) {
+ const pressed = useSharedValue(0)
+
+ const tap = Gesture.Tap()
+ .onBegin(() => { pressed.set(withTiming(1)) })
+ .onFinalize(() => { pressed.set(withTiming(0)) })
+ .onEnd(() => { runOnJS(onPress)() })
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
+ }))
+
+ return (
+
+
+ Press me
+
+
+ )
+}
+```
+
+---
+
+## 4. Scroll Performance
+
+**Impact: HIGH**
+
+Tracking scroll position without causing render thrashing.
+
+### 4.1 Never Track Scroll Position in useState
+
+**Impact: HIGH (prevents render thrashing during scroll)**
+
+Never store scroll position in `useState`. Use a Reanimated shared value for animations or a ref for non-reactive tracking.
+
+**Incorrect:**
+
+```tsx
+const [scrollY, setScrollY] = useState(0)
+
+const onScroll = (e) => {
+ setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
+}
+```
+
+**Correct (Reanimated for animations):**
+
+```tsx
+const scrollY = useSharedValue(0)
+
+const onScroll = useAnimatedScrollHandler({
+ onScroll: (e) => {
+ scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
+ },
+})
+
+return
+```
+
+---
+
+## 5. Navigation
+
+**Impact: HIGH**
+
+Using Expo Router and native navigators for file-based routing on iOS and Android.
+
+### 5.1 Use Expo Router for File-Based Navigation
+
+**Impact: HIGH (native performance, platform-appropriate UI)**
+
+Use Expo Router for file-based navigation. It uses native navigators under the hood.
+
+**Stack Navigation:**
+
+```tsx
+// app/_layout.tsx
+import { Stack } from 'expo-router'
+
+export default function RootLayout() {
+ return (
+
+
+
+
+ )
+}
+```
+
+**Tab Navigation:**
+
+```tsx
+// app/(tabs)/_layout.tsx
+import { Tabs } from 'expo-router'
+
+export default function TabLayout() {
+ return (
+
+ }} />
+ }} />
+ }} />
+
+ )
+}
+```
+
+**Programmatic Navigation:**
+
+```tsx
+import { router } from 'expo-router'
+
+router.push('/transaction-details')
+router.push({ pathname: '/transaction-details', params: { txid: '...' } })
+router.back()
+router.replace('/home')
+```
+
+---
+
+## 6. React State
+
+**Impact: MEDIUM**
+
+Patterns for managing React state to avoid stale closures and unnecessary re-renders.
+
+### 6.1 Minimize State Variables and Derive Values
+
+**Impact: MEDIUM (fewer re-renders, less state drift)**
+
+If a value can be computed from existing state or props, derive it during render instead of storing it in state.
+
+**Incorrect:**
+
+```tsx
+const [total, setTotal] = useState(0)
+const [itemCount, setItemCount] = useState(0)
+
+useEffect(() => {
+ setTotal(items.reduce((sum, item) => sum + item.price, 0))
+ setItemCount(items.length)
+}, [items])
+```
+
+**Correct:**
+
+```tsx
+const total = items.reduce((sum, item) => sum + item.price, 0)
+const itemCount = items.length
+```
+
+### 6.2 Use Fallback State Instead of initialState
+
+**Impact: MEDIUM (reactive fallbacks without syncing)**
+
+Use `undefined` as initial state and nullish coalescing to fall back to parent or server values.
+
+**Correct:**
+
+```tsx
+function Toggle({ fallbackEnabled }: Props) {
+ const [_enabled, setEnabled] = useState(undefined)
+ const enabled = _enabled ?? fallbackEnabled
+
+ return
+}
+```
+
+### 6.3 Use Dispatch Updaters for State That Depends on Current Value
+
+**Impact: MEDIUM (avoids stale closures)**
+
+When the next state depends on the current state, use a dispatch updater.
+
+**Incorrect:**
+
+```tsx
+const onTap = () => {
+ setCount(count + 1) // count may be stale
+}
+```
+
+**Correct:**
+
+```tsx
+const onTap = () => {
+ setCount((prev) => prev + 1)
+}
+```
+
+---
+
+## 7. State Architecture
+
+**Impact: MEDIUM**
+
+Ground truth principles for state variables and derived values.
+
+### 7.1 State Must Represent Ground Truth
+
+**Impact: HIGH (cleaner logic, easier debugging, single source of truth)**
+
+State variables should represent the actual state (e.g., `pressed`, `isOpen`), not derived visual values (e.g., `scale`, `opacity`).
+
+**Incorrect:**
+
+```tsx
+const scale = useSharedValue(1)
+
+const tap = Gesture.Tap()
+ .onBegin(() => { scale.set(withTiming(0.95)) })
+ .onFinalize(() => { scale.set(withTiming(1)) })
+```
+
+**Correct:**
+
+```tsx
+const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
+
+const tap = Gesture.Tap()
+ .onBegin(() => { pressed.set(withTiming(1)) })
+ .onFinalize(() => { pressed.set(withTiming(0)) })
+
+const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
+}))
+```
+
+---
+
+## 8. React Compiler
+
+**Impact: MEDIUM**
+
+Compatibility patterns for React Compiler with React Native and Reanimated.
+
+### 8.1 Destructure Functions Early in Render
+
+**Impact: HIGH (stable references, fewer re-renders)**
+
+Destructure functions from hooks at the top of render scope.
+
+**Incorrect:**
+
+```tsx
+function SaveButton(props) {
+ const router = useRouter()
+
+ const handlePress = () => {
+ props.onSave()
+ router.push('/success') // unstable reference
+ }
+}
+```
+
+**Correct:**
+
+```tsx
+function SaveButton({ onSave }) {
+ const { push } = useRouter()
+
+ const handlePress = () => {
+ onSave()
+ push('/success') // stable reference
+ }
+}
+```
+
+### 8.2 Use .get() and .set() for Reanimated Shared Values
+
+**Impact: LOW (required for React Compiler compatibility)**
+
+With React Compiler enabled, use `.get()` and `.set()` instead of `.value`.
+
+**Incorrect:**
+
+```tsx
+count.value = count.value + 1
+```
+
+**Correct:**
+
+```tsx
+count.set(count.get() + 1)
+```
+
+---
+
+## 9. User Interface
+
+**Impact: MEDIUM**
+
+Native UI patterns for images, menus, modals, styling with NativeWind, and platform-consistent interfaces.
+
+### 9.1 Use expo-image for Optimized Images
+
+**Impact: HIGH (memory efficiency, caching, blurhash placeholders)**
+
+Use `expo-image` instead of React Native's `Image`.
+
+**Correct:**
+
+```tsx
+import { Image } from 'expo-image'
+
+
+```
+
+### 9.2 Styling with NativeWind and Modern Patterns
+
+**Impact: MEDIUM (consistent design, cleaner layouts)**
+
+This project uses NativeWind (Tailwind CSS for React Native).
+
+**Basic usage:**
+
+```tsx
+
+ {title}
+
+```
+
+**Use gap for spacing:**
+
+```tsx
+
+ Title
+ Subtitle
+
+```
+
+**Platform-specific styles:**
+
+```tsx
+
+```
+
+**Dark mode:**
+
+```tsx
+
+ Content
+
+```
+
+### 9.3 Use Pressable Instead of Touchable Components
+
+**Impact: LOW (modern API, more flexible)**
+
+Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` instead.
+
+**Correct:**
+
+```tsx
+import { Pressable } from 'react-native'
+
+
+ Press me
+
+```
+
+### 9.4 Use contentInsetAdjustmentBehavior for Safe Areas
+
+**Impact: MEDIUM (native safe area handling)**
+
+Use `contentInsetAdjustmentBehavior="automatic"` on ScrollView instead of SafeAreaView.
+
+**Correct:**
+
+```tsx
+
+
+ Content
+
+
+```
+
+### 9.5 Use contentInset for Dynamic ScrollView Spacing
+
+**Impact: LOW (smoother updates, no layout recalculation)**
+
+Use `contentInset` instead of padding for dynamic spacing.
+
+**Correct:**
+
+```tsx
+
+ {children}
+
+```
+
+### 9.6 Use Native Menus for Dropdowns and Context Menus
+
+**Impact: HIGH (native accessibility, platform-consistent UX)**
+
+Use native platform menus instead of custom JS implementations.
+
+**Correct (with zeego):**
+
+```tsx
+import * as DropdownMenu from 'zeego/dropdown-menu'
+
+
+
+ Open Menu
+
+
+ console.log('edit')}>
+ Edit
+
+
+
+```
+
+### 9.7 Use Native Modals Over JS-Based Bottom Sheets
+
+**Impact: HIGH (native performance, gestures, accessibility)**
+
+Use native `` with `presentationStyle="formSheet"` instead of JS-based bottom sheets.
+
+**Correct:**
+
+```tsx
+ setVisible(false)}
+>
+ Sheet content
+
+```
+
+### 9.8 Measuring View Dimensions
+
+**Impact: MEDIUM (synchronous measurement)**
+
+Use both `useLayoutEffect` and `onLayout` for measuring views.
+
+**Correct:**
+
+```tsx
+const ref = useRef(null)
+const [size, setSize] = useState(undefined)
+
+useLayoutEffect(() => {
+ const rect = ref.current?.getBoundingClientRect()
+ if (rect) setSize({ width: rect.width, height: rect.height })
+}, [])
+
+const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ setSize((prev) => {
+ if (prev?.width === width && prev?.height === height) return prev
+ return { width, height }
+ })
+}
+
+return {children}
+```
+
+---
+
+## 10. JavaScript
+
+**Impact: LOW**
+
+Micro-optimizations like hoisting expensive object creation.
+
+### 10.1 Hoist Intl Formatter Creation
+
+**Impact: LOW-MEDIUM (avoids expensive object recreation)**
+
+Don't create `Intl.DateTimeFormat` or `Intl.NumberFormat` inside render. Hoist to module scope.
+
+**Incorrect:**
+
+```tsx
+function Price({ amount }: { amount: number }) {
+ const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
+ return {formatter.format(amount)}
+}
+```
+
+**Correct:**
+
+```tsx
+const currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
+
+function Price({ amount }: { amount: number }) {
+ return {currencyFormatter.format(amount)}
+}
+```
+
+---
+
+## 11. Fonts
+
+**Impact: LOW**
+
+Native font loading for improved performance.
+
+### 11.1 Load Fonts Natively at Build Time
+
+**Impact: LOW (fonts available at launch, no async loading)**
+
+Use the `expo-font` config plugin to embed fonts at build time.
+
+**Correct (app.json):**
+
+```json
+{
+ "expo": {
+ "plugins": [
+ ["expo-font", { "fonts": ["./assets/fonts/Geist-Bold.otf"] }]
+ ]
+ }
+}
+```
+
+```tsx
+// No loading state needed—font is already available
+Hello
+```
+
+After adding fonts, run `npx expo prebuild` and rebuild the native app.
+
+---
+
+## References
+
+1. [React Documentation](https://react.dev)
+2. [React Native Documentation](https://reactnative.dev)
+3. [Expo Documentation](https://docs.expo.dev)
+4. [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated)
+5. [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler)
+6. [NativeWind Documentation](https://www.nativewind.dev)
diff --git a/.github/skills/react-native-skills/README.md b/.github/skills/react-native-skills/README.md
new file mode 100644
index 0000000..83121c1
--- /dev/null
+++ b/.github/skills/react-native-skills/README.md
@@ -0,0 +1,132 @@
+# React Native & Expo Skills
+
+Best practices for React Native and Expo mobile applications targeting iOS and
+Android. Optimized for AI agents and LLMs working on this codebase.
+
+## Tech Stack
+
+- React Native 0.81+ with New Architecture
+- Expo SDK 54+
+- Expo Router 5.1 (file-based navigation)
+- NativeWind 4.1 (Tailwind CSS)
+- React Native Reanimated 4.1
+- TypeScript 5.9
+
+## Structure
+
+- `rules/` - Individual rule files (one per rule)
+ - `_sections.md` - Section metadata (titles, impacts, descriptions)
+ - `_template.md` - Template for creating new rules
+ - `{prefix}-{description}.md` - Individual rule files
+- `metadata.json` - Document metadata (version, organization, abstract)
+- `SKILL.md` - Skill overview and quick reference
+- **`AGENTS.md`** - Compiled output for agents (generated)
+
+## Rules by Category
+
+### Core Rendering (CRITICAL)
+
+- `rendering-text-in-text-component.md` - Wrap strings in Text components
+- `rendering-no-falsy-and.md` - Avoid falsy && operator in JSX
+
+### List Performance (HIGH)
+
+- `list-performance-virtualize.md` - Use FlashList for lists
+- `list-performance-function-references.md` - Keep stable object references
+- `list-performance-callbacks.md` - Hoist callbacks to list root
+- `list-performance-inline-objects.md` - Avoid inline objects in renderItem
+- `list-performance-item-memo.md` - Pass primitives for memoization
+- `list-performance-item-expensive.md` - Keep list items lightweight
+- `list-performance-images.md` - Use compressed images in lists
+- `list-performance-item-types.md` - Use item types for heterogeneous lists
+
+### Animation (HIGH)
+
+- `animation-gpu-properties.md` - Animate transform/opacity only
+- `animation-gesture-detector-press.md` - Use GestureDetector for press
+- `animation-derived-value.md` - Prefer useDerivedValue
+
+### Scroll Performance (HIGH)
+
+- `scroll-position-no-state.md` - Never track scroll in useState
+
+### Navigation (HIGH)
+
+- `navigation-native-navigators.md` - Use Expo Router
+
+### React State (MEDIUM)
+
+- `react-state-dispatcher.md` - Use functional setState updates
+- `react-state-fallback.md` - State represents user intent only
+- `react-state-minimize.md` - Minimize state, derive values
+
+### State Architecture (MEDIUM)
+
+- `state-ground-truth.md` - State must represent ground truth
+
+### React Compiler (MEDIUM)
+
+- `react-compiler-destructure-functions.md` - Destructure functions early
+- `react-compiler-reanimated-shared-values.md` - Use .get()/.set()
+
+### User Interface (MEDIUM)
+
+- `ui-expo-image.md` - Use expo-image for images
+- `ui-menus.md` - Native dropdown and context menus
+- `ui-native-modals.md` - Use native Modal with formSheet
+- `ui-pressable.md` - Use Pressable over TouchableOpacity
+- `ui-measure-views.md` - Measuring view dimensions
+- `ui-safe-area-scroll.md` - Use contentInsetAdjustmentBehavior
+- `ui-scrollview-content-inset.md` - Use contentInset for spacing
+- `ui-styling.md` - NativeWind styling patterns
+
+### JavaScript (LOW)
+
+- `js-hoist-intl.md` - Hoist Intl formatter creation
+
+### Fonts (LOW)
+
+- `fonts-config-plugin.md` - Load fonts natively at build time
+
+## Creating a New Rule
+
+1. Copy `rules/_template.md` to `rules/{prefix}-{description}.md`
+2. Choose the appropriate prefix from `_sections.md`
+3. Fill in the frontmatter and content
+4. Include clear incorrect/correct examples
+
+## Rule File Structure
+
+```markdown
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description
+tags: tag1, tag2, tag3
+---
+
+## Rule Title Here
+
+Brief explanation of the rule and why it matters.
+
+**Incorrect (description):**
+
+\`\`\`tsx
+// Bad code example
+\`\`\`
+
+**Correct (description):**
+
+\`\`\`tsx
+// Good code example
+\`\`\`
+
+Reference: [Link](https://example.com)
+```
+
+## Impact Levels
+
+- `CRITICAL` - Causes crashes or broken UI
+- `HIGH` - Significant performance improvements
+- `MEDIUM` - Moderate improvements
+- `LOW` - Incremental improvements
diff --git a/.github/skills/react-native-skills/SKILL.md b/.github/skills/react-native-skills/SKILL.md
new file mode 100644
index 0000000..5043c19
--- /dev/null
+++ b/.github/skills/react-native-skills/SKILL.md
@@ -0,0 +1,142 @@
+---
+name: react-native-expo-skills
+description:
+ React Native and Expo best practices for building performant iOS and Android
+ mobile apps. Use when building components, optimizing list performance,
+ implementing animations with Reanimated, or styling with NativeWind. Triggers
+ on tasks involving React Native, Expo, Expo Router navigation, or mobile
+ performance optimization.
+license: MIT
+metadata:
+ author: BitSleuth
+ version: '2.0.0'
+---
+
+# React Native & Expo Skills
+
+Best practices for React Native and Expo mobile applications targeting iOS and
+Android. Contains rules covering performance, animations, navigation, styling,
+and platform-specific optimizations.
+
+## Tech Stack
+
+- React Native 0.81+
+- Expo SDK 54+
+- Expo Router (file-based navigation)
+- NativeWind (Tailwind CSS)
+- React Native Reanimated
+- TypeScript
+
+## When to Apply
+
+Reference these guidelines when:
+
+- Building React Native or Expo apps for iOS/Android
+- Optimizing list and scroll performance
+- Implementing animations with Reanimated
+- Configuring navigation with Expo Router
+- Styling components with NativeWind
+- Working with images using expo-image
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix |
+| -------- | ---------------- | -------- | ------------------- |
+| 1 | Core Rendering | CRITICAL | `rendering-` |
+| 2 | List Performance | HIGH | `list-performance-` |
+| 3 | Animation | HIGH | `animation-` |
+| 4 | Scroll | HIGH | `scroll-` |
+| 5 | Navigation | HIGH | `navigation-` |
+| 6 | React State | MEDIUM | `react-state-` |
+| 7 | State Arch | MEDIUM | `state-` |
+| 8 | React Compiler | MEDIUM | `react-compiler-` |
+| 9 | User Interface | MEDIUM | `ui-` |
+| 10 | JavaScript | LOW | `js-` |
+| 11 | Fonts | LOW | `fonts-` |
+
+## Quick Reference
+
+### 1. Core Rendering (CRITICAL)
+
+- `rendering-text-in-text-component` - Wrap strings in Text components
+- `rendering-no-falsy-and` - Avoid falsy && for conditional rendering
+
+### 2. List Performance (HIGH)
+
+- `list-performance-virtualize` - Use FlashList for large lists
+- `list-performance-item-memo` - Pass primitives for memoization
+- `list-performance-callbacks` - Hoist callbacks to list root
+- `list-performance-inline-objects` - Avoid inline style objects
+- `list-performance-function-references` - Keep stable object references
+- `list-performance-images` - Use compressed images in lists
+- `list-performance-item-expensive` - Keep list items lightweight
+- `list-performance-item-types` - Use item types for heterogeneous lists
+
+### 3. Animation (HIGH)
+
+- `animation-gpu-properties` - Animate transform and opacity only
+- `animation-derived-value` - Use useDerivedValue for computed animations
+- `animation-gesture-detector-press` - Use Gesture.Tap for press animations
+
+### 4. Scroll Performance (HIGH)
+
+- `scroll-position-no-state` - Never track scroll position in useState
+
+### 5. Navigation (HIGH)
+
+- `navigation-native-navigators` - Use Expo Router for file-based navigation
+
+### 6. React State (MEDIUM)
+
+- `react-state-minimize` - Minimize state variables, derive values
+- `react-state-dispatcher` - Use functional setState updates
+- `react-state-fallback` - Use fallback state instead of initialState
+
+### 7. State Architecture (MEDIUM)
+
+- `state-ground-truth` - State must represent ground truth
+
+### 8. React Compiler (MEDIUM)
+
+- `react-compiler-destructure-functions` - Destructure functions early
+- `react-compiler-reanimated-shared-values` - Use .get()/.set() for shared values
+
+### 9. User Interface (MEDIUM)
+
+- `ui-expo-image` - Use expo-image for all images
+- `ui-pressable` - Use Pressable over TouchableOpacity
+- `ui-safe-area-scroll` - Use contentInsetAdjustmentBehavior
+- `ui-scrollview-content-inset` - Use contentInset for dynamic spacing
+- `ui-menus` - Use native context menus
+- `ui-native-modals` - Use native modals when possible
+- `ui-measure-views` - Use onLayout for measuring views
+- `ui-styling` - Use NativeWind for styling
+
+### 10. JavaScript (LOW)
+
+- `js-hoist-intl` - Hoist Intl object creation
+
+### 11. Fonts (LOW)
+
+- `fonts-config-plugin` - Use config plugins for custom fonts
+
+## How to Use
+
+Read individual rule files for detailed explanations and code examples:
+
+```
+rules/list-performance-virtualize.md
+rules/animation-gpu-properties.md
+rules/ui-styling.md
+```
+
+Each rule file contains:
+
+- Brief explanation of why it matters
+- Incorrect code example with explanation
+- Correct code example with explanation
+- Additional context and references
+
+## Full Compiled Document
+
+For the complete guide with all rules expanded: `AGENTS.md`
diff --git a/.github/skills/react-native-skills/metadata.json b/.github/skills/react-native-skills/metadata.json
new file mode 100644
index 0000000..37ab176
--- /dev/null
+++ b/.github/skills/react-native-skills/metadata.json
@@ -0,0 +1,14 @@
+{
+ "version": "2.0.0",
+ "organization": "BitSleuth",
+ "date": "January 2026",
+ "abstract": "Performance and best practices guide for React Native and Expo mobile applications targeting iOS and Android. Contains 25+ rules across 11 categories covering core rendering, list performance, animations with Reanimated, navigation with Expo Router, styling with NativeWind, and platform-specific optimizations. Each rule includes detailed explanations, incorrect vs. correct code examples, and impact assessments.",
+ "references": [
+ "https://react.dev",
+ "https://reactnative.dev",
+ "https://docs.expo.dev",
+ "https://docs.swmansion.com/react-native-reanimated",
+ "https://docs.swmansion.com/react-native-gesture-handler",
+ "https://www.nativewind.dev"
+ ]
+}
diff --git a/.github/skills/react-native-skills/rules/_sections.md b/.github/skills/react-native-skills/rules/_sections.md
new file mode 100644
index 0000000..8fba312
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/_sections.md
@@ -0,0 +1,68 @@
+# Sections
+
+This file defines all sections, their ordering, impact levels, and descriptions.
+The section ID (in parentheses) is the filename prefix used to group rules.
+
+---
+
+## 1. Core Rendering (rendering)
+
+**Impact:** CRITICAL
+**Description:** Fundamental React Native rendering rules. Violations cause
+runtime crashes or broken UI.
+
+## 2. List Performance (list-performance)
+
+**Impact:** HIGH
+**Description:** Optimizing virtualized lists (FlatList, FlashList) for smooth
+scrolling and fast updates.
+
+## 3. Animation (animation)
+
+**Impact:** HIGH
+**Description:** GPU-accelerated animations, Reanimated patterns, and avoiding
+render thrashing during gestures.
+
+## 4. Scroll Performance (scroll)
+
+**Impact:** HIGH
+**Description:** Tracking scroll position without causing render thrashing.
+
+## 5. Navigation (navigation)
+
+**Impact:** HIGH
+**Description:** Using Expo Router and native navigators for file-based routing
+on iOS and Android.
+
+## 6. React State (react-state)
+
+**Impact:** MEDIUM
+**Description:** Patterns for managing React state to avoid stale closures and
+unnecessary re-renders.
+
+## 7. State Architecture (state)
+
+**Impact:** MEDIUM
+**Description:** Ground truth principles for state variables and derived values.
+
+## 8. React Compiler (react-compiler)
+
+**Impact:** MEDIUM
+**Description:** Compatibility patterns for React Compiler with React Native and
+Reanimated.
+
+## 9. User Interface (ui)
+
+**Impact:** MEDIUM
+**Description:** Native UI patterns for images, menus, modals, styling with
+NativeWind, and platform-consistent interfaces.
+
+## 10. JavaScript (js)
+
+**Impact:** LOW
+**Description:** Micro-optimizations like hoisting expensive object creation.
+
+## 11. Fonts (fonts)
+
+**Impact:** LOW
+**Description:** Native font loading for improved performance.
diff --git a/.github/skills/react-native-skills/rules/_template.md b/.github/skills/react-native-skills/rules/_template.md
new file mode 100644
index 0000000..1e9e707
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/_template.md
@@ -0,0 +1,28 @@
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description of impact (e.g., "20-50% improvement")
+tags: tag1, tag2
+---
+
+## Rule Title Here
+
+**Impact: MEDIUM (optional impact description)**
+
+Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
+
+**Incorrect (description of what's wrong):**
+
+```typescript
+// Bad code example here
+const bad = example()
+```
+
+**Correct (description of what's right):**
+
+```typescript
+// Good code example here
+const good = example()
+```
+
+Reference: [Link to documentation or resource](https://example.com)
diff --git a/.github/skills/react-native-skills/rules/animation-derived-value.md b/.github/skills/react-native-skills/rules/animation-derived-value.md
new file mode 100644
index 0000000..310928a
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/animation-derived-value.md
@@ -0,0 +1,53 @@
+---
+title: Prefer useDerivedValue Over useAnimatedReaction
+impact: MEDIUM
+impactDescription: cleaner code, automatic dependency tracking
+tags: animation, reanimated, derived-value
+---
+
+## Prefer useDerivedValue Over useAnimatedReaction
+
+When deriving a shared value from another, use `useDerivedValue` instead of
+`useAnimatedReaction`. Derived values are declarative, automatically track
+dependencies, and return a value you can use directly. Animated reactions are
+for side effects, not derivations.
+
+**Incorrect (useAnimatedReaction for derivation):**
+
+```tsx
+import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated'
+
+function MyComponent() {
+ const progress = useSharedValue(0)
+ const opacity = useSharedValue(1)
+
+ useAnimatedReaction(
+ () => progress.value,
+ (current) => {
+ opacity.value = 1 - current
+ }
+ )
+
+ // ...
+}
+```
+
+**Correct (useDerivedValue):**
+
+```tsx
+import { useSharedValue, useDerivedValue } from 'react-native-reanimated'
+
+function MyComponent() {
+ const progress = useSharedValue(0)
+
+ const opacity = useDerivedValue(() => 1 - progress.get())
+
+ // ...
+}
+```
+
+Use `useAnimatedReaction` only for side effects that don't produce a value
+(e.g., triggering haptics, logging, calling `runOnJS`).
+
+Reference:
+[Reanimated useDerivedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue)
diff --git a/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md b/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md
new file mode 100644
index 0000000..87c6782
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/animation-gesture-detector-press.md
@@ -0,0 +1,95 @@
+---
+title: Use GestureDetector for Animated Press States
+impact: MEDIUM
+impactDescription: UI thread animations, smoother press feedback
+tags: animation, gestures, press, reanimated
+---
+
+## Use GestureDetector for Animated Press States
+
+For animated press states (scale, opacity on press), use `GestureDetector` with
+`Gesture.Tap()` and shared values instead of Pressable's
+`onPressIn`/`onPressOut`. Gesture callbacks run on the UI thread as worklets—no
+JS thread round-trip for press animations.
+
+**Incorrect (Pressable with JS thread callbacks):**
+
+```tsx
+import { Pressable } from 'react-native'
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+} from 'react-native-reanimated'
+
+function AnimatedButton({ onPress }: { onPress: () => void }) {
+ const scale = useSharedValue(1)
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }],
+ }))
+
+ return (
+ (scale.value = withTiming(0.95))}
+ onPressOut={() => (scale.value = withTiming(1))}
+ >
+
+ Press me
+
+
+ )
+}
+```
+
+**Correct (GestureDetector with UI thread worklets):**
+
+```tsx
+import { Gesture, GestureDetector } from 'react-native-gesture-handler'
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ interpolate,
+ runOnJS,
+} from 'react-native-reanimated'
+
+function AnimatedButton({ onPress }: { onPress: () => void }) {
+ // Store the press STATE (0 = not pressed, 1 = pressed)
+ const pressed = useSharedValue(0)
+
+ const tap = Gesture.Tap()
+ .onBegin(() => {
+ pressed.set(withTiming(1))
+ })
+ .onFinalize(() => {
+ pressed.set(withTiming(0))
+ })
+ .onEnd(() => {
+ runOnJS(onPress)()
+ })
+
+ // Derive visual values from the state
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ { scale: interpolate(withTiming(pressed.get()), [0, 1], [1, 0.95]) },
+ ],
+ }))
+
+ return (
+
+
+ Press me
+
+
+ )
+}
+```
+
+Store the press **state** (0 or 1), then derive the scale via `interpolate`.
+This keeps the shared value as ground truth. Use `runOnJS` to call JS functions
+from worklets. Use `.set()` and `.get()` for React Compiler compatibility.
+
+Reference:
+[Gesture Handler Tap Gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture)
diff --git a/.github/skills/react-native-skills/rules/animation-gpu-properties.md b/.github/skills/react-native-skills/rules/animation-gpu-properties.md
new file mode 100644
index 0000000..5fda095
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/animation-gpu-properties.md
@@ -0,0 +1,65 @@
+---
+title: Animate Transform and Opacity Instead of Layout Properties
+impact: HIGH
+impactDescription: GPU-accelerated animations, no layout recalculation
+tags: animation, performance, reanimated, transform, opacity
+---
+
+## Animate Transform and Opacity Instead of Layout Properties
+
+Avoid animating `width`, `height`, `top`, `left`, `margin`, or `padding`. These trigger layout recalculation on every frame. Instead, use `transform` (scale, translate) and `opacity` which run on the GPU without triggering layout.
+
+**Incorrect (animates height, triggers layout every frame):**
+
+```tsx
+import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
+
+function CollapsiblePanel({ expanded }: { expanded: boolean }) {
+ const animatedStyle = useAnimatedStyle(() => ({
+ height: withTiming(expanded ? 200 : 0), // triggers layout on every frame
+ overflow: 'hidden',
+ }))
+
+ return {children}
+}
+```
+
+**Correct (animates scaleY, GPU-accelerated):**
+
+```tsx
+import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
+
+function CollapsiblePanel({ expanded }: { expanded: boolean }) {
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ { scaleY: withTiming(expanded ? 1 : 0) },
+ ],
+ opacity: withTiming(expanded ? 1 : 0),
+ }))
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Correct (animates translateY for slide animations):**
+
+```tsx
+import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
+
+function SlideIn({ visible }: { visible: boolean }) {
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ { translateY: withTiming(visible ? 0 : 100) },
+ ],
+ opacity: withTiming(visible ? 1 : 0),
+ }))
+
+ return {children}
+}
+```
+
+GPU-accelerated properties: `transform` (translate, scale, rotate), `opacity`. Everything else triggers layout.
diff --git a/.github/skills/react-native-skills/rules/fonts-config-plugin.md b/.github/skills/react-native-skills/rules/fonts-config-plugin.md
new file mode 100644
index 0000000..39aa014
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/fonts-config-plugin.md
@@ -0,0 +1,71 @@
+---
+title: Load fonts natively at build time
+impact: LOW
+impactDescription: fonts available at launch, no async loading
+tags: fonts, expo, performance, config-plugin
+---
+
+## Use Expo Config Plugin for Font Loading
+
+Use the `expo-font` config plugin to embed fonts at build time instead of
+`useFonts` or `Font.loadAsync`. Embedded fonts are more efficient.
+
+**Incorrect (async font loading):**
+
+```tsx
+import { useFonts } from 'expo-font'
+import { Text, View } from 'react-native'
+
+function App() {
+ const [fontsLoaded] = useFonts({
+ 'Geist-Bold': require('./assets/fonts/Geist-Bold.otf'),
+ })
+
+ if (!fontsLoaded) {
+ return null
+ }
+
+ return (
+
+ Hello
+
+ )
+}
+```
+
+**Correct (config plugin, fonts embedded at build):**
+
+```json
+// app.json
+{
+ "expo": {
+ "plugins": [
+ [
+ "expo-font",
+ {
+ "fonts": ["./assets/fonts/Geist-Bold.otf"]
+ }
+ ]
+ ]
+ }
+}
+```
+
+```tsx
+import { Text, View } from 'react-native'
+
+function App() {
+ // No loading state needed—font is already available
+ return (
+
+ Hello
+
+ )
+}
+```
+
+After adding fonts to the config plugin, run `npx expo prebuild` and rebuild the
+native app.
+
+Reference:
+[Expo Font Documentation](https://docs.expo.dev/versions/latest/sdk/font/)
diff --git a/.github/skills/react-native-skills/rules/js-hoist-intl.md b/.github/skills/react-native-skills/rules/js-hoist-intl.md
new file mode 100644
index 0000000..9af1c35
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/js-hoist-intl.md
@@ -0,0 +1,61 @@
+---
+title: Hoist Intl Formatter Creation
+impact: LOW-MEDIUM
+impactDescription: avoids expensive object recreation
+tags: javascript, intl, optimization, memoization
+---
+
+## Hoist Intl Formatter Creation
+
+Don't create `Intl.DateTimeFormat`, `Intl.NumberFormat`, or
+`Intl.RelativeTimeFormat` inside render or loops. These are expensive to
+instantiate. Hoist to module scope when the locale/options are static.
+
+**Incorrect (new formatter every render):**
+
+```tsx
+function Price({ amount }: { amount: number }) {
+ const formatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ })
+ return {formatter.format(amount)}
+}
+```
+
+**Correct (hoisted to module scope):**
+
+```tsx
+const currencyFormatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+})
+
+function Price({ amount }: { amount: number }) {
+ return {currencyFormatter.format(amount)}
+}
+```
+
+**For dynamic locales, memoize:**
+
+```tsx
+const dateFormatter = useMemo(
+ () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }),
+ [locale]
+)
+```
+
+**Common formatters to hoist:**
+
+```tsx
+// Module-level formatters
+const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' })
+const timeFormatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short' })
+const percentFormatter = new Intl.NumberFormat('en-US', { style: 'percent' })
+const relativeFormatter = new Intl.RelativeTimeFormat('en-US', {
+ numeric: 'auto',
+})
+```
+
+Creating `Intl` objects is significantly more expensive than `RegExp` or plain
+objects—each instantiation parses locale data and builds internal lookup tables.
diff --git a/.github/skills/react-native-skills/rules/list-performance-callbacks.md b/.github/skills/react-native-skills/rules/list-performance-callbacks.md
new file mode 100644
index 0000000..a0b3913
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-callbacks.md
@@ -0,0 +1,44 @@
+---
+title: Hoist callbacks to the root of lists
+impact: MEDIUM
+impactDescription: Fewer re-renders and faster lists
+tags: tag1, tag2
+---
+
+## List performance callbacks
+
+**Impact: HIGH (Fewer re-renders and faster lists)**
+
+When passing callback functions to list items, create a single instance of the
+callback at the root of the list. Items should then call it with a unique
+identifier.
+
+**Incorrect (creates a new callback on each render):**
+
+```typescript
+return (
+ {
+ // bad: creates a new callback on each render
+ const onPress = () => handlePress(item.id)
+ return
+ }}
+ />
+)
+```
+
+**Correct (a single function instance passed to each item):**
+
+```typescript
+const onPress = useCallback(() => handlePress(item.id), [handlePress, item.id])
+
+return (
+ (
+
+ )}
+ />
+)
+```
+
+Reference: [Link to documentation or resource](https://example.com)
diff --git a/.github/skills/react-native-skills/rules/list-performance-function-references.md b/.github/skills/react-native-skills/rules/list-performance-function-references.md
new file mode 100644
index 0000000..9721929
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-function-references.md
@@ -0,0 +1,132 @@
+---
+title: Optimize List Performance with Stable Object References
+impact: CRITICAL
+impactDescription: virtualization relies on reference stability
+tags: lists, performance, flatlist, virtualization
+---
+
+## Optimize List Performance with Stable Object References
+
+Don't map or filter data before passing to virtualized lists. Virtualization
+relies on object reference stability to know what changed—new references cause
+full re-renders of all visible items. Attempt to prevent frequent renders at the
+list-parent level.
+
+Where needed, use context selectors within list items.
+
+**Incorrect (creates new object references on every keystroke):**
+
+```tsx
+function DomainSearch() {
+ const { keyword, setKeyword } = useKeywordZustandState()
+ const { data: tlds } = useTlds()
+
+ // Bad: creates new objects on every render, reparenting the entire list on every keystroke
+ const domains = tlds.map((tld) => ({
+ domain: `${keyword}.${tld.name}`,
+ tld: tld.name,
+ price: tld.price,
+ }))
+
+ return (
+ <>
+
+ }
+ />
+ >
+ )
+}
+```
+
+**Correct (stable references, transform inside items):**
+
+```tsx
+const renderItem = ({ item }) =>
+
+function DomainSearch() {
+ const { data: tlds } = useTlds()
+
+ return (
+
+ )
+}
+
+function DomainItem({ tld }: { tld: Tld }) {
+ // good: transform within items, and don't pass the dynamic data as a prop
+ // good: use a selector function from zustand to receive a stable string back
+ const domain = useKeywordZustandState((s) => s.keyword + '.' + tld.name)
+ return {domain}
+}
+```
+
+**Updating parent array reference:**
+
+Creating a new array instance can be okay, as long as its inner object
+references are stable. For instance, if you sort a list of objects:
+
+```tsx
+// good: creates a new array instance without mutating the inner objects
+// good: parent array reference is unaffected by typing and updating "keyword"
+const sortedTlds = tlds.toSorted((a, b) => a.name.localeCompare(b.name))
+
+return
+```
+
+Even though this creates a new array instance `sortedTlds`, the inner object
+references are stable.
+
+**With zustand for dynamic data (avoids parent re-renders):**
+
+```tsx
+const useSearchStore = create<{ keyword: string }>(() => ({ keyword: '' }))
+
+function DomainSearch() {
+ const { data: tlds } = useTlds()
+
+ return (
+ <>
+
+ }
+ />
+ >
+ )
+}
+
+function DomainItem({ tld }: { tld: Tld }) {
+ // Select only what you need—component only re-renders when keyword changes
+ const keyword = useSearchStore((s) => s.keyword)
+ const domain = `${keyword}.${tld.name}`
+ return {domain}
+}
+```
+
+Virtualization can now skip items that haven't changed when typing. Only visible
+items (~20) re-render on keystroke, rather than the parent.
+
+**Deriving state within list items based on parent data (avoids parent
+re-renders):**
+
+For components where the data is conditional based on the parent state, this
+pattern is even more important. For example, if you are checking if an item is
+favorited, toggling favorites only re-renders one component if the item itself
+is in charge of accessing the state rather than the parent:
+
+```tsx
+function DomainItemFavoriteButton({ tld }: { tld: Tld }) {
+ const isFavorited = useFavoritesStore((s) => s.favorites.has(tld.id))
+ return
+}
+```
+
+Note: if you're using the React Compiler, you can read React Context values
+directly within list items. Although this is slightly slower than using a
+Zustand selector in most cases, the effect may be negligible.
diff --git a/.github/skills/react-native-skills/rules/list-performance-images.md b/.github/skills/react-native-skills/rules/list-performance-images.md
new file mode 100644
index 0000000..75a3baf
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-images.md
@@ -0,0 +1,53 @@
+---
+title: Use Compressed Images in Lists
+impact: HIGH
+impactDescription: faster load times, less memory
+tags: lists, images, performance, optimization
+---
+
+## Use Compressed Images in Lists
+
+Always load compressed, appropriately-sized images in lists. Full-resolution
+images consume excessive memory and cause scroll jank. Request thumbnails from
+your server or use an image CDN with resize parameters.
+
+**Incorrect (full-resolution images):**
+
+```tsx
+function ProductItem({ product }: { product: Product }) {
+ return (
+
+ {/* 4000x3000 image loaded for a 100x100 thumbnail */}
+
+ {product.name}
+
+ )
+}
+```
+
+**Correct (request appropriately-sized image):**
+
+```tsx
+function ProductItem({ product }: { product: Product }) {
+ // Request a 200x200 image (2x for retina)
+ const thumbnailUrl = `${product.imageUrl}?w=200&h=200&fit=cover`
+
+ return (
+
+
+ {product.name}
+
+ )
+}
+```
+
+Use an optimized image component with built-in caching and placeholder support,
+such as `expo-image` or `SolitoImage` (which uses `expo-image` under the hood).
+Request images at 2x the display size for retina screens.
diff --git a/.github/skills/react-native-skills/rules/list-performance-inline-objects.md b/.github/skills/react-native-skills/rules/list-performance-inline-objects.md
new file mode 100644
index 0000000..d5b6514
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-inline-objects.md
@@ -0,0 +1,97 @@
+---
+title: Avoid Inline Objects in renderItem
+impact: HIGH
+impactDescription: prevents unnecessary re-renders of memoized list items
+tags: lists, performance, flatlist, virtualization, memo
+---
+
+## Avoid Inline Objects in renderItem
+
+Don't create new objects inside `renderItem` to pass as props. Inline objects
+create new references on every render, breaking memoization. Pass primitive
+values directly from `item` instead.
+
+**Incorrect (inline object breaks memoization):**
+
+```tsx
+function UserList({ users }: { users: User[] }) {
+ return (
+ (
+
+ )}
+ />
+ )
+}
+```
+
+**Incorrect (inline style object):**
+
+```tsx
+renderItem={({ item }) => (
+
+)}
+```
+
+**Correct (pass item directly or primitives):**
+
+```tsx
+function UserList({ users }: { users: User[] }) {
+ return (
+ (
+ // Good: pass the item directly
+
+ )}
+ />
+ )
+}
+```
+
+**Correct (pass primitives, derive inside child):**
+
+```tsx
+renderItem={({ item }) => (
+
+)}
+
+const UserRow = memo(function UserRow({ id, name, isActive }: Props) {
+ // Good: derive style inside memoized component
+ const backgroundColor = isActive ? 'green' : 'gray'
+ return {/* ... */}
+})
+```
+
+**Correct (hoist static styles in module scope):**
+
+```tsx
+const activeStyle = { backgroundColor: 'green' }
+const inactiveStyle = { backgroundColor: 'gray' }
+
+renderItem={({ item }) => (
+
+)}
+```
+
+Passing primitives or stable references allows `memo()` to skip re-renders when
+the actual values haven't changed.
+
+**Note:** If you have the React Compiler enabled, it handles memoization
+automatically and these manual optimizations become less critical.
diff --git a/.github/skills/react-native-skills/rules/list-performance-item-expensive.md b/.github/skills/react-native-skills/rules/list-performance-item-expensive.md
new file mode 100644
index 0000000..f617a76
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-item-expensive.md
@@ -0,0 +1,94 @@
+---
+title: Keep List Items Lightweight
+impact: HIGH
+impactDescription: reduces render time for visible items during scroll
+tags: lists, performance, virtualization, hooks
+---
+
+## Keep List Items Lightweight
+
+List items should be as inexpensive as possible to render. Minimize hooks, avoid
+queries, and limit React Context access. Virtualized lists render many items
+during scroll—expensive items cause jank.
+
+**Incorrect (heavy list item):**
+
+```tsx
+function ProductRow({ id }: { id: string }) {
+ // Bad: query inside list item
+ const { data: product } = useQuery(['product', id], () => fetchProduct(id))
+ // Bad: multiple context accesses
+ const theme = useContext(ThemeContext)
+ const user = useContext(UserContext)
+ const cart = useContext(CartContext)
+ // Bad: expensive computation
+ const recommendations = useMemo(
+ () => computeRecommendations(product),
+ [product]
+ )
+
+ return {/* ... */}
+}
+```
+
+**Correct (lightweight list item):**
+
+```tsx
+function ProductRow({ name, price, imageUrl }: Props) {
+ // Good: receives only primitives, minimal hooks
+ return (
+
+
+ {name}
+ {price}
+
+ )
+}
+```
+
+**Move data fetching to parent:**
+
+```tsx
+// Parent fetches all data once
+function ProductList() {
+ const { data: products } = useQuery(['products'], fetchProducts)
+
+ return (
+ (
+
+ )}
+ />
+ )
+}
+```
+
+**For shared values, use Zustand selectors instead of Context:**
+
+```tsx
+// Incorrect: Context causes re-render when any cart value changes
+function ProductRow({ id, name }: Props) {
+ const { items } = useContext(CartContext)
+ const inCart = items.includes(id)
+ // ...
+}
+
+// Correct: Zustand selector only re-renders when this specific value changes
+function ProductRow({ id, name }: Props) {
+ // use Set.has (created once at the root) instead of Array.includes()
+ const inCart = useCartStore((s) => s.items.has(id))
+ // ...
+}
+```
+
+**Guidelines for list items:**
+
+- No queries or data fetching
+- No expensive computations (move to parent or memoize at parent level)
+- Prefer Zustand selectors over React Context
+- Minimize useState/useEffect hooks
+- Pass pre-computed values as props
+
+The goal: list items should be simple rendering functions that take props and
+return JSX.
diff --git a/.github/skills/react-native-skills/rules/list-performance-item-memo.md b/.github/skills/react-native-skills/rules/list-performance-item-memo.md
new file mode 100644
index 0000000..634935e
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-item-memo.md
@@ -0,0 +1,82 @@
+---
+title: Pass Primitives to List Items for Memoization
+impact: HIGH
+impactDescription: enables effective memo() comparison
+tags: lists, performance, memo, primitives
+---
+
+## Pass Primitives to List Items for Memoization
+
+When possible, pass only primitive values (strings, numbers, booleans) as props
+to list item components. Primitives enable shallow comparison in `memo()` to
+work correctly, skipping re-renders when values haven't changed.
+
+**Incorrect (object prop requires deep comparison):**
+
+```tsx
+type User = { id: string; name: string; email: string; avatar: string }
+
+const UserRow = memo(function UserRow({ user }: { user: User }) {
+ // memo() compares user by reference, not value
+ // If parent creates new user object, this re-renders even if data is same
+ return {user.name}
+})
+
+renderItem={({ item }) => }
+```
+
+This can still be optimized, but it is harder to memoize properly.
+
+**Correct (primitive props enable shallow comparison):**
+
+```tsx
+const UserRow = memo(function UserRow({
+ id,
+ name,
+ email,
+}: {
+ id: string
+ name: string
+ email: string
+}) {
+ // memo() compares each primitive directly
+ // Re-renders only if id, name, or email actually changed
+ return {name}
+})
+
+renderItem={({ item }) => (
+
+)}
+```
+
+**Pass only what you need:**
+
+```tsx
+// Incorrect: passing entire item when you only need name
+
+
+// Correct: pass only the fields the component uses
+
+```
+
+**For callbacks, hoist or use item ID:**
+
+```tsx
+// Incorrect: inline function creates new reference
+ handlePress(item.id)} />
+
+// Correct: pass ID, handle in child
+
+
+const UserRow = memo(function UserRow({ id, name }: Props) {
+ const handlePress = useCallback(() => {
+ // use id here
+ }, [id])
+ return {name}
+})
+```
+
+Primitive props make memoization predictable and effective.
+
+**Note:** If you have the React Compiler enabled, you do not need to use
+`memo()` or `useCallback()`, but the object references still apply.
diff --git a/.github/skills/react-native-skills/rules/list-performance-item-types.md b/.github/skills/react-native-skills/rules/list-performance-item-types.md
new file mode 100644
index 0000000..1027e4e
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-item-types.md
@@ -0,0 +1,104 @@
+---
+title: Use Item Types for Heterogeneous Lists
+impact: HIGH
+impactDescription: efficient recycling, less layout thrashing
+tags: list, performance, recycling, heterogeneous, LegendList
+---
+
+## Use Item Types for Heterogeneous Lists
+
+When a list has different item layouts (messages, images, headers, etc.), use a
+`type` field on each item and provide `getItemType` to the list. This puts items
+into separate recycling pools so a message component never gets recycled into an
+image component.
+
+**Incorrect (single component with conditionals):**
+
+```tsx
+type Item = { id: string; text?: string; imageUrl?: string; isHeader?: boolean }
+
+function ListItem({ item }: { item: Item }) {
+ if (item.isHeader) {
+ return
+ }
+ if (item.imageUrl) {
+ return
+ }
+ return
+}
+
+function Feed({ items }: { items: Item[] }) {
+ return (
+ }
+ recycleItems
+ />
+ )
+}
+```
+
+**Correct (typed items with separate components):**
+
+```tsx
+type HeaderItem = { id: string; type: 'header'; title: string }
+type MessageItem = { id: string; type: 'message'; text: string }
+type ImageItem = { id: string; type: 'image'; url: string }
+type FeedItem = HeaderItem | MessageItem | ImageItem
+
+function Feed({ items }: { items: FeedItem[] }) {
+ return (
+ item.id}
+ getItemType={(item) => item.type}
+ renderItem={({ item }) => {
+ switch (item.type) {
+ case 'header':
+ return
+ case 'message':
+ return
+ case 'image':
+ return
+ }
+ }}
+ recycleItems
+ />
+ )
+}
+```
+
+**Why this matters:**
+
+- **Recycling efficiency**: Items with the same type share a recycling pool
+- **No layout thrashing**: A header never recycles into an image cell
+- **Type safety**: TypeScript can narrow the item type in each branch
+- **Better size estimation**: Use `getEstimatedItemSize` with `itemType` for
+ accurate estimates per type
+
+```tsx
+ item.id}
+ getItemType={(item) => item.type}
+ getEstimatedItemSize={(index, item, itemType) => {
+ switch (itemType) {
+ case 'header':
+ return 48
+ case 'message':
+ return 72
+ case 'image':
+ return 300
+ default:
+ return 72
+ }
+ }}
+ renderItem={({ item }) => {
+ /* ... */
+ }}
+ recycleItems
+/>
+```
+
+Reference:
+[LegendList getItemType](https://legendapp.com/open-source/list/api/props/#getitemtype-v2)
diff --git a/.github/skills/react-native-skills/rules/list-performance-virtualize.md b/.github/skills/react-native-skills/rules/list-performance-virtualize.md
new file mode 100644
index 0000000..8a393ba
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/list-performance-virtualize.md
@@ -0,0 +1,67 @@
+---
+title: Use a List Virtualizer for Any List
+impact: HIGH
+impactDescription: reduced memory, faster mounts
+tags: lists, performance, virtualization, scrollview
+---
+
+## Use a List Virtualizer for Any List
+
+Use a list virtualizer like LegendList or FlashList instead of ScrollView with
+mapped children—even for short lists. Virtualizers only render visible items,
+reducing memory usage and mount time. ScrollView renders all children upfront,
+which gets expensive quickly.
+
+**Incorrect (ScrollView renders all items at once):**
+
+```tsx
+function Feed({ items }: { items: Item[] }) {
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ )
+}
+// 50 items = 50 components mounted, even if only 10 visible
+```
+
+**Correct (virtualizer renders only visible items):**
+
+```tsx
+import { LegendList } from '@legendapp/list'
+
+function Feed({ items }: { items: Item[] }) {
+ return (
+ }
+ keyExtractor={(item) => item.id}
+ estimatedItemSize={80}
+ />
+ )
+}
+// Only ~10-15 visible items mounted at a time
+```
+
+**Alternative (FlashList):**
+
+```tsx
+import { FlashList } from '@shopify/flash-list'
+
+function Feed({ items }: { items: Item[] }) {
+ return (
+ }
+ keyExtractor={(item) => item.id}
+ />
+ )
+}
+```
+
+Benefits apply to any screen with scrollable content—profiles, settings, feeds,
+search results. Default to virtualization.
diff --git a/.github/skills/react-native-skills/rules/navigation-native-navigators.md b/.github/skills/react-native-skills/rules/navigation-native-navigators.md
new file mode 100644
index 0000000..ad60a54
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/navigation-native-navigators.md
@@ -0,0 +1,153 @@
+---
+title: Use Expo Router for File-Based Navigation
+impact: HIGH
+impactDescription: native performance, platform-appropriate UI
+tags: navigation, expo-router, native-stack, tabs, ios, android
+---
+
+## Use Expo Router for File-Based Navigation
+
+Use Expo Router for file-based navigation. Expo Router uses native navigators
+under the hood (UINavigationController on iOS, Fragment on Android) for better
+performance and native behavior.
+
+### Stack Navigation
+
+Expo Router uses native stack by default:
+
+```tsx
+// app/_layout.tsx
+import { Stack } from 'expo-router'
+
+export default function RootLayout() {
+ return (
+
+
+
+
+ )
+}
+```
+
+### Tab Navigation
+
+Use the Tabs component for bottom tab navigation:
+
+```tsx
+// app/(tabs)/_layout.tsx
+import { Tabs } from 'expo-router'
+
+export default function TabLayout() {
+ return (
+
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+
+ )
+}
+```
+
+### Modal Screens
+
+Use presentation: 'modal' for modal screens:
+
+```tsx
+
+```
+
+### Prefer Native Header Options Over Custom Components
+
+**Incorrect (custom header component):**
+
+```tsx
+ ,
+ }}
+/>
+```
+
+**Correct (native header options):**
+
+```tsx
+
+```
+
+Native headers support iOS large titles, search bars, blur effects, and proper
+safe area handling automatically.
+
+### Programmatic Navigation
+
+```tsx
+import { router } from 'expo-router'
+
+// Navigate to a screen
+router.push('/transaction-details')
+
+// Navigate with params
+router.push({
+ pathname: '/transaction-details',
+ params: { txid: '...' },
+})
+
+// Go back
+router.back()
+
+// Replace current screen
+router.replace('/home')
+```
+
+### Why Expo Router
+
+- **File-based routing**: Routes are defined by file structure
+- **Native performance**: Uses native stack and tab navigators
+- **Type safety**: Full TypeScript support for routes
+- **Deep linking**: Built-in deep linking support
+- **Platform behavior**: Automatic iOS/Android platform conventions
+
+Reference:
+
+- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
+- [Expo Router Native Tabs](https://docs.expo.dev/router/advanced/native-tabs/)
diff --git a/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md b/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md
new file mode 100644
index 0000000..f76c25a
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/react-compiler-destructure-functions.md
@@ -0,0 +1,50 @@
+---
+title: Destructure Functions Early in Render (React Compiler)
+impact: HIGH
+impactDescription: stable references, fewer re-renders
+tags: rerender, hooks, performance, react-compiler
+---
+
+## Destructure Functions Early in Render
+
+This rule is only applicable if you are using the React Compiler.
+
+Destructure functions from hooks at the top of render scope. Never dot into
+objects to call functions. Destructured functions are stable references; dotting
+creates new references and breaks memoization.
+
+**Incorrect (dotting into object):**
+
+```tsx
+import { useRouter } from 'expo-router'
+
+function SaveButton(props) {
+ const router = useRouter()
+
+ // bad: react-compiler will key the cache on "props" and "router", which are objects that change each render
+ const handlePress = () => {
+ props.onSave()
+ router.push('/success') // unstable reference
+ }
+
+ return
+}
+```
+
+**Correct (destructure early):**
+
+```tsx
+import { useRouter } from 'expo-router'
+
+function SaveButton({ onSave }) {
+ const { push } = useRouter()
+
+ // good: react-compiler will key on push and onSave
+ const handlePress = () => {
+ onSave()
+ push('/success') // stable reference
+ }
+
+ return
+}
+```
diff --git a/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md b/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md
new file mode 100644
index 0000000..0dcbaf4
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/react-compiler-reanimated-shared-values.md
@@ -0,0 +1,48 @@
+---
+title: Use .get() and .set() for Reanimated Shared Values (not .value)
+impact: LOW
+impactDescription: required for React Compiler compatibility
+tags: reanimated, react-compiler, shared-values
+---
+
+## Use .get() and .set() for Shared Values with React Compiler
+
+With React Compiler enabled, use `.get()` and `.set()` instead of reading or
+writing `.value` directly on Reanimated shared values. The compiler can't track
+property access—explicit methods ensure correct behavior.
+
+**Incorrect (breaks with React Compiler):**
+
+```tsx
+import { useSharedValue } from 'react-native-reanimated'
+
+function Counter() {
+ const count = useSharedValue(0)
+
+ const increment = () => {
+ count.value = count.value + 1 // opts out of react compiler
+ }
+
+ return
+}
+```
+
+**Correct (React Compiler compatible):**
+
+```tsx
+import { useSharedValue } from 'react-native-reanimated'
+
+function Counter() {
+ const count = useSharedValue(0)
+
+ const increment = () => {
+ count.set(count.get() + 1)
+ }
+
+ return
+}
+```
+
+See the
+[Reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support)
+for more.
diff --git a/.github/skills/react-native-skills/rules/react-state-dispatcher.md b/.github/skills/react-native-skills/rules/react-state-dispatcher.md
new file mode 100644
index 0000000..93e8b6d
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/react-state-dispatcher.md
@@ -0,0 +1,91 @@
+---
+title: useState Dispatch updaters for State That Depends on Current Value
+impact: MEDIUM
+impactDescription: avoids stale closures, prevents unnecessary re-renders
+tags: state, hooks, useState, callbacks
+---
+
+## Use Dispatch Updaters for State That Depends on Current Value
+
+When the next state depends on the current state, use a dispatch updater
+(`setState(prev => ...)`) instead of reading the state variable directly in a
+callback. This avoids stale closures and ensures you're comparing against the
+latest value.
+
+**Incorrect (reads state directly):**
+
+```tsx
+const [size, setSize] = useState(undefined)
+
+const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ // size may be stale in this closure
+ if (size?.width !== width || size?.height !== height) {
+ setSize({ width, height })
+ }
+}
+```
+
+**Correct (dispatch updater):**
+
+```tsx
+const [size, setSize] = useState(undefined)
+
+const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ setSize((prev) => {
+ if (prev?.width === width && prev?.height === height) return prev
+ return { width, height }
+ })
+}
+```
+
+Returning the previous value from the updater skips the re-render.
+
+For primitive states, you don't need to compare values before firing a
+re-render.
+
+**Incorrect (unnecessary comparison for primitive state):**
+
+```tsx
+const [size, setSize] = useState(undefined)
+
+const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ setSize((prev) => (prev === width ? prev : width))
+}
+```
+
+**Correct (sets primitive state directly):**
+
+```tsx
+const [size, setSize] = useState(undefined)
+
+const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ setSize(width)
+}
+```
+
+However, if the next state depends on the current state, you should still use a
+dispatch updater.
+
+**Incorrect (reads state directly from the callback):**
+
+```tsx
+const [count, setCount] = useState(0)
+
+const onTap = () => {
+ setCount(count + 1)
+}
+```
+
+**Correct (dispatch updater):**
+
+```tsx
+const [count, setCount] = useState(0)
+
+const onTap = () => {
+ setCount((prev) => prev + 1)
+}
+```
diff --git a/.github/skills/react-native-skills/rules/react-state-fallback.md b/.github/skills/react-native-skills/rules/react-state-fallback.md
new file mode 100644
index 0000000..204f346
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/react-state-fallback.md
@@ -0,0 +1,56 @@
+---
+title: Use fallback state instead of initialState
+impact: MEDIUM
+impactDescription: reactive fallbacks without syncing
+tags: state, hooks, derived-state, props, initialState
+---
+
+## Use fallback state instead of initialState
+
+Use `undefined` as initial state and nullish coalescing (`??`) to fall back to
+parent or server values. State represents user intent only—`undefined` means
+"user hasn't chosen yet." This enables reactive fallbacks that update when the
+source changes, not just on initial render.
+
+**Incorrect (syncs state, loses reactivity):**
+
+```tsx
+type Props = { fallbackEnabled: boolean }
+
+function Toggle({ fallbackEnabled }: Props) {
+ const [enabled, setEnabled] = useState(defaultEnabled)
+ // If fallbackEnabled changes, state is stale
+ // State mixes user intent with default value
+
+ return
+}
+```
+
+**Correct (state is user intent, reactive fallback):**
+
+```tsx
+type Props = { fallbackEnabled: boolean }
+
+function Toggle({ fallbackEnabled }: Props) {
+ const [_enabled, setEnabled] = useState(undefined)
+ const enabled = _enabled ?? defaultEnabled
+ // undefined = user hasn't touched it, falls back to prop
+ // If defaultEnabled changes, component reflects it
+ // Once user interacts, their choice persists
+
+ return
+}
+```
+
+**With server data:**
+
+```tsx
+function ProfileForm({ data }: { data: User }) {
+ const [_theme, setTheme] = useState(undefined)
+ const theme = _theme ?? data.theme
+ // Shows server value until user overrides
+ // Server refetch updates the fallback automatically
+
+ return
+}
+```
diff --git a/.github/skills/react-native-skills/rules/react-state-minimize.md b/.github/skills/react-native-skills/rules/react-state-minimize.md
new file mode 100644
index 0000000..64605b6
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/react-state-minimize.md
@@ -0,0 +1,65 @@
+---
+title: Minimize State Variables and Derive Values
+impact: MEDIUM
+impactDescription: fewer re-renders, less state drift
+tags: state, derived-state, hooks, optimization
+---
+
+## Minimize State Variables and Derive Values
+
+Use the fewest state variables possible. If a value can be computed from existing state or props, derive it during render instead of storing it in state. Redundant state causes unnecessary re-renders and can drift out of sync.
+
+**Incorrect (redundant state):**
+
+```tsx
+function Cart({ items }: { items: Item[] }) {
+ const [total, setTotal] = useState(0)
+ const [itemCount, setItemCount] = useState(0)
+
+ useEffect(() => {
+ setTotal(items.reduce((sum, item) => sum + item.price, 0))
+ setItemCount(items.length)
+ }, [items])
+
+ return (
+
+ {itemCount} items
+ Total: ${total}
+
+ )
+}
+```
+
+**Correct (derived values):**
+
+```tsx
+function Cart({ items }: { items: Item[] }) {
+ const total = items.reduce((sum, item) => sum + item.price, 0)
+ const itemCount = items.length
+
+ return (
+
+ {itemCount} items
+ Total: ${total}
+
+ )
+}
+```
+
+**Another example:**
+
+```tsx
+// Incorrect: storing both firstName, lastName, AND fullName
+const [firstName, setFirstName] = useState('')
+const [lastName, setLastName] = useState('')
+const [fullName, setFullName] = useState('')
+
+// Correct: derive fullName
+const [firstName, setFirstName] = useState('')
+const [lastName, setLastName] = useState('')
+const fullName = `${firstName} ${lastName}`
+```
+
+State should be the minimal source of truth. Everything else is derived.
+
+Reference: [Choosing the State Structure](https://react.dev/learn/choosing-the-state-structure)
diff --git a/.github/skills/react-native-skills/rules/rendering-no-falsy-and.md b/.github/skills/react-native-skills/rules/rendering-no-falsy-and.md
new file mode 100644
index 0000000..30f05d3
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/rendering-no-falsy-and.md
@@ -0,0 +1,74 @@
+---
+title: Never Use && with Potentially Falsy Values
+impact: CRITICAL
+impactDescription: prevents production crash
+tags: rendering, conditional, jsx, crash
+---
+
+## Never Use && with Potentially Falsy Values
+
+Never use `{value && }` when `value` could be an empty string or
+`0`. These are falsy but JSX-renderable—React Native will try to render them as
+text outside a `` component, causing a hard crash in production.
+
+**Incorrect (crashes if count is 0 or name is ""):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {name && {name}}
+ {count && {count} items}
+
+ )
+}
+// If name="" or count=0, renders the falsy value → crash
+```
+
+**Correct (ternary with null):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {name ? {name} : null}
+ {count ? {count} items : null}
+
+ )
+}
+```
+
+**Correct (explicit boolean coercion):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ return (
+
+ {!!name && {name}}
+ {!!count && {count} items}
+
+ )
+}
+```
+
+**Best (early return):**
+
+```tsx
+function Profile({ name, count }: { name: string; count: number }) {
+ if (!name) return null
+
+ return (
+
+ {name}
+ {count > 0 ? {count} items : null}
+
+ )
+}
+```
+
+Early returns are clearest. When using conditionals inline, prefer ternary or
+explicit boolean checks.
+
+**Lint rule:** Enable `react/jsx-no-leaked-render` from
+[eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md)
+to catch this automatically.
diff --git a/.github/skills/react-native-skills/rules/rendering-text-in-text-component.md b/.github/skills/react-native-skills/rules/rendering-text-in-text-component.md
new file mode 100644
index 0000000..fd1b9f4
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/rendering-text-in-text-component.md
@@ -0,0 +1,36 @@
+---
+title: Wrap Strings in Text Components
+impact: CRITICAL
+impactDescription: prevents runtime crash
+tags: rendering, text, core
+---
+
+## Wrap Strings in Text Components
+
+Strings must be rendered inside ``. React Native crashes if a string is a
+direct child of ``.
+
+**Incorrect (crashes):**
+
+```tsx
+import { View } from 'react-native'
+
+function Greeting({ name }: { name: string }) {
+ return Hello, {name}!
+}
+// Error: Text strings must be rendered within a component.
+```
+
+**Correct:**
+
+```tsx
+import { View, Text } from 'react-native'
+
+function Greeting({ name }: { name: string }) {
+ return (
+
+ Hello, {name}!
+
+ )
+}
+```
diff --git a/.github/skills/react-native-skills/rules/scroll-position-no-state.md b/.github/skills/react-native-skills/rules/scroll-position-no-state.md
new file mode 100644
index 0000000..a5760cd
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/scroll-position-no-state.md
@@ -0,0 +1,82 @@
+---
+title: Never Track Scroll Position in useState
+impact: HIGH
+impactDescription: prevents render thrashing during scroll
+tags: scroll, performance, reanimated, useRef
+---
+
+## Never Track Scroll Position in useState
+
+Never store scroll position in `useState`. Scroll events fire rapidly—state
+updates cause render thrashing and dropped frames. Use a Reanimated shared value
+for animations or a ref for non-reactive tracking.
+
+**Incorrect (useState causes jank):**
+
+```tsx
+import { useState } from 'react'
+import {
+ ScrollView,
+ NativeSyntheticEvent,
+ NativeScrollEvent,
+} from 'react-native'
+
+function Feed() {
+ const [scrollY, setScrollY] = useState(0)
+
+ const onScroll = (e: NativeSyntheticEvent) => {
+ setScrollY(e.nativeEvent.contentOffset.y) // re-renders on every frame
+ }
+
+ return
+}
+```
+
+**Correct (Reanimated for animations):**
+
+```tsx
+import Animated, {
+ useSharedValue,
+ useAnimatedScrollHandler,
+} from 'react-native-reanimated'
+
+function Feed() {
+ const scrollY = useSharedValue(0)
+
+ const onScroll = useAnimatedScrollHandler({
+ onScroll: (e) => {
+ scrollY.value = e.contentOffset.y // runs on UI thread, no re-render
+ },
+ })
+
+ return (
+
+ )
+}
+```
+
+**Correct (ref for non-reactive tracking):**
+
+```tsx
+import { useRef } from 'react'
+import {
+ ScrollView,
+ NativeSyntheticEvent,
+ NativeScrollEvent,
+} from 'react-native'
+
+function Feed() {
+ const scrollY = useRef(0)
+
+ const onScroll = (e: NativeSyntheticEvent) => {
+ scrollY.current = e.nativeEvent.contentOffset.y // no re-render
+ }
+
+ return
+}
+```
diff --git a/.github/skills/react-native-skills/rules/state-ground-truth.md b/.github/skills/react-native-skills/rules/state-ground-truth.md
new file mode 100644
index 0000000..c3c4bd9
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/state-ground-truth.md
@@ -0,0 +1,80 @@
+---
+title: State Must Represent Ground Truth
+impact: HIGH
+impactDescription: cleaner logic, easier debugging, single source of truth
+tags: state, derived-state, reanimated, hooks
+---
+
+## State Must Represent Ground Truth
+
+State variables—both React `useState` and Reanimated shared values—should
+represent the actual state of something (e.g., `pressed`, `progress`, `isOpen`),
+not derived visual values (e.g., `scale`, `opacity`, `translateY`). Derive
+visual values from state using computation or interpolation.
+
+**Incorrect (storing the visual output):**
+
+```tsx
+const scale = useSharedValue(1)
+
+const tap = Gesture.Tap()
+ .onBegin(() => {
+ scale.set(withTiming(0.95))
+ })
+ .onFinalize(() => {
+ scale.set(withTiming(1))
+ })
+
+const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.get() }],
+}))
+```
+
+**Correct (storing the state, deriving the visual):**
+
+```tsx
+const pressed = useSharedValue(0) // 0 = not pressed, 1 = pressed
+
+const tap = Gesture.Tap()
+ .onBegin(() => {
+ pressed.set(withTiming(1))
+ })
+ .onFinalize(() => {
+ pressed.set(withTiming(0))
+ })
+
+const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: interpolate(pressed.get(), [0, 1], [1, 0.95]) }],
+}))
+```
+
+**Why this matters:**
+
+State variables should represent real "state", not necessarily a desired end
+result.
+
+1. **Single source of truth** — The state (`pressed`) describes what's
+ happening; visuals are derived
+2. **Easier to extend** — Adding opacity, rotation, or other effects just
+ requires more interpolations from the same state
+3. **Debugging** — Inspecting `pressed = 1` is clearer than `scale = 0.95`
+4. **Reusable logic** — The same `pressed` value can drive multiple visual
+ properties
+
+**Same principle for React state:**
+
+```tsx
+// Incorrect: storing derived values
+const [isExpanded, setIsExpanded] = useState(false)
+const [height, setHeight] = useState(0)
+
+useEffect(() => {
+ setHeight(isExpanded ? 200 : 0)
+}, [isExpanded])
+
+// Correct: derive from state
+const [isExpanded, setIsExpanded] = useState(false)
+const height = isExpanded ? 200 : 0
+```
+
+State is the minimal truth. Everything else is derived.
diff --git a/.github/skills/react-native-skills/rules/ui-expo-image.md b/.github/skills/react-native-skills/rules/ui-expo-image.md
new file mode 100644
index 0000000..72d768f
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-expo-image.md
@@ -0,0 +1,66 @@
+---
+title: Use expo-image for Optimized Images
+impact: HIGH
+impactDescription: memory efficiency, caching, blurhash placeholders, progressive loading
+tags: images, performance, expo-image, ui
+---
+
+## Use expo-image for Optimized Images
+
+Use `expo-image` instead of React Native's `Image`. It provides memory-efficient caching, blurhash placeholders, progressive loading, and better performance for lists.
+
+**Incorrect (React Native Image):**
+
+```tsx
+import { Image } from 'react-native'
+
+function Avatar({ url }: { url: string }) {
+ return
+}
+```
+
+**Correct (expo-image):**
+
+```tsx
+import { Image } from 'expo-image'
+
+function Avatar({ url }: { url: string }) {
+ return
+}
+```
+
+**With blurhash placeholder:**
+
+```tsx
+
+```
+
+**With priority and caching:**
+
+```tsx
+
+```
+
+**Key props:**
+
+- `placeholder` — Blurhash or thumbnail while loading
+- `contentFit` — `cover`, `contain`, `fill`, `scale-down`
+- `transition` — Fade-in duration (ms)
+- `priority` — `low`, `normal`, `high`
+- `cachePolicy` — `memory`, `disk`, `memory-disk`, `none`
+- `recyclingKey` — Unique key for list recycling
+
+For cross-platform (web + native), use `SolitoImage` from `solito/image` which uses `expo-image` under the hood.
+
+Reference: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)
diff --git a/.github/skills/react-native-skills/rules/ui-measure-views.md b/.github/skills/react-native-skills/rules/ui-measure-views.md
new file mode 100644
index 0000000..8b783fe
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-measure-views.md
@@ -0,0 +1,78 @@
+---
+title: Measuring View Dimensions
+impact: MEDIUM
+impactDescription: synchronous measurement, avoid unnecessary re-renders
+tags: layout, measurement, onLayout, useLayoutEffect
+---
+
+## Measuring View Dimensions
+
+Use both `useLayoutEffect` (synchronous) and `onLayout` (for updates). The sync
+measurement gives you the initial size immediately; `onLayout` keeps it current
+when the view changes. For non-primitive states, use a dispatch updater to
+compare values and avoid unnecessary re-renders.
+
+**Height only:**
+
+```tsx
+import { useLayoutEffect, useRef, useState } from 'react'
+import { View, LayoutChangeEvent } from 'react-native'
+
+function MeasuredBox({ children }: { children: React.ReactNode }) {
+ const ref = useRef(null)
+ const [height, setHeight] = useState(undefined)
+
+ useLayoutEffect(() => {
+ // Sync measurement on mount (RN 0.82+)
+ const rect = ref.current?.getBoundingClientRect()
+ if (rect) setHeight(rect.height)
+ // Pre-0.82: ref.current?.measure((x, y, w, h) => setHeight(h))
+ }, [])
+
+ const onLayout = (e: LayoutChangeEvent) => {
+ setHeight(e.nativeEvent.layout.height)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Both dimensions:**
+
+```tsx
+import { useLayoutEffect, useRef, useState } from 'react'
+import { View, LayoutChangeEvent } from 'react-native'
+
+type Size = { width: number; height: number }
+
+function MeasuredBox({ children }: { children: React.ReactNode }) {
+ const ref = useRef(null)
+ const [size, setSize] = useState(undefined)
+
+ useLayoutEffect(() => {
+ const rect = ref.current?.getBoundingClientRect()
+ if (rect) setSize({ width: rect.width, height: rect.height })
+ }, [])
+
+ const onLayout = (e: LayoutChangeEvent) => {
+ const { width, height } = e.nativeEvent.layout
+ setSize((prev) => {
+ // for non-primitive states, compare values before firing a re-render
+ if (prev?.width === width && prev?.height === height) return prev
+ return { width, height }
+ })
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Use functional setState to compare—don't read state directly in the callback.
diff --git a/.github/skills/react-native-skills/rules/ui-menus.md b/.github/skills/react-native-skills/rules/ui-menus.md
new file mode 100644
index 0000000..5168bc2
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-menus.md
@@ -0,0 +1,174 @@
+---
+title: Use Native Menus for Dropdowns and Context Menus
+impact: HIGH
+impactDescription: native accessibility, platform-consistent UX
+tags: user-interface, menus, context-menus, zeego, accessibility
+---
+
+## Use Native Menus for Dropdowns and Context Menus
+
+Use native platform menus instead of custom JS implementations. Native menus
+provide built-in accessibility, consistent platform UX, and better performance.
+Use [zeego](https://zeego.dev) for cross-platform native menus.
+
+**Incorrect (custom JS menu):**
+
+```tsx
+import { useState } from 'react'
+import { View, Pressable, Text } from 'react-native'
+
+function MyMenu() {
+ const [open, setOpen] = useState(false)
+
+ return (
+
+ setOpen(!open)}>
+ Open Menu
+
+ {open && (
+
+ console.log('edit')}>
+ Edit
+
+ console.log('delete')}>
+ Delete
+
+
+ )}
+
+ )
+}
+```
+
+**Correct (native menu with zeego):**
+
+```tsx
+import * as DropdownMenu from 'zeego/dropdown-menu'
+
+function MyMenu() {
+ return (
+
+
+
+ Open Menu
+
+
+
+
+ console.log('edit')}>
+ Edit
+
+
+ console.log('delete')}
+ >
+ Delete
+
+
+
+ )
+}
+```
+
+**Context menu (long-press):**
+
+```tsx
+import * as ContextMenu from 'zeego/context-menu'
+
+function MyContextMenu() {
+ return (
+
+
+
+ Long press me
+
+
+
+
+ console.log('copy')}>
+ Copy
+
+
+ console.log('paste')}>
+ Paste
+
+
+
+ )
+}
+```
+
+**Checkbox items:**
+
+```tsx
+import * as DropdownMenu from 'zeego/dropdown-menu'
+
+function SettingsMenu() {
+ const [notifications, setNotifications] = useState(true)
+
+ return (
+
+
+
+ Settings
+
+
+
+
+ setNotifications((prev) => !prev)}
+ >
+
+ Notifications
+
+
+
+ )
+}
+```
+
+**Submenus:**
+
+```tsx
+import * as DropdownMenu from 'zeego/dropdown-menu'
+
+function MenuWithSubmenu() {
+ return (
+
+
+
+ Options
+
+
+
+
+ console.log('home')}>
+ Home
+
+
+
+
+ More Options
+
+
+
+
+ Settings
+
+
+
+ Help
+
+
+
+
+
+ )
+}
+```
+
+Reference: [Zeego Documentation](https://zeego.dev/components/dropdown-menu)
diff --git a/.github/skills/react-native-skills/rules/ui-native-modals.md b/.github/skills/react-native-skills/rules/ui-native-modals.md
new file mode 100644
index 0000000..f560e11
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-native-modals.md
@@ -0,0 +1,77 @@
+---
+title: Use Native Modals Over JS-Based Bottom Sheets
+impact: HIGH
+impactDescription: native performance, gestures, accessibility
+tags: modals, bottom-sheet, native, react-navigation
+---
+
+## Use Native Modals Over JS-Based Bottom Sheets
+
+Use native `` with `presentationStyle="formSheet"` or React Navigation
+v7's native form sheet instead of JS-based bottom sheet libraries. Native modals
+have built-in gestures, accessibility, and better performance. Rely on native UI
+for low-level primitives.
+
+**Incorrect (JS-based bottom sheet):**
+
+```tsx
+import BottomSheet from 'custom-js-bottom-sheet'
+
+function MyScreen() {
+ const sheetRef = useRef(null)
+
+ return (
+
+
+ )
+}
+```
+
+**Correct (native Modal with formSheet):**
+
+```tsx
+import { Modal, View, Text, Button } from 'react-native'
+
+function MyScreen() {
+ const [visible, setVisible] = useState(false)
+
+ return (
+
+
+ )
+}
+```
+
+**Correct (React Navigation v7 native form sheet):**
+
+```tsx
+// In your navigator
+
+```
+
+Native modals provide swipe-to-dismiss, proper keyboard avoidance, and
+accessibility out of the box.
diff --git a/.github/skills/react-native-skills/rules/ui-pressable.md b/.github/skills/react-native-skills/rules/ui-pressable.md
new file mode 100644
index 0000000..31c3d20
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-pressable.md
@@ -0,0 +1,61 @@
+---
+title: Use Pressable Instead of Touchable Components
+impact: LOW
+impactDescription: modern API, more flexible
+tags: ui, pressable, touchable, gestures
+---
+
+## Use Pressable Instead of Touchable Components
+
+Never use `TouchableOpacity` or `TouchableHighlight`. Use `Pressable` from
+`react-native` or `react-native-gesture-handler` instead.
+
+**Incorrect (legacy Touchable components):**
+
+```tsx
+import { TouchableOpacity } from 'react-native'
+
+function MyButton({ onPress }: { onPress: () => void }) {
+ return (
+
+ Press me
+
+ )
+}
+```
+
+**Correct (Pressable):**
+
+```tsx
+import { Pressable } from 'react-native'
+
+function MyButton({ onPress }: { onPress: () => void }) {
+ return (
+
+ Press me
+
+ )
+}
+```
+
+**Correct (Pressable from gesture handler for lists):**
+
+```tsx
+import { Pressable } from 'react-native-gesture-handler'
+
+function ListItem({ onPress }: { onPress: () => void }) {
+ return (
+
+ Item
+
+ )
+}
+```
+
+Use `react-native-gesture-handler` Pressable inside scrollable lists for better
+gesture coordination, as long as you are using the ScrollView from
+`react-native-gesture-handler` as well.
+
+**For animated press states (scale, opacity changes):** Use `GestureDetector`
+with Reanimated shared values instead of Pressable's style callback. See the
+`animation-gesture-detector-press` rule.
diff --git a/.github/skills/react-native-skills/rules/ui-safe-area-scroll.md b/.github/skills/react-native-skills/rules/ui-safe-area-scroll.md
new file mode 100644
index 0000000..79812bc
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-safe-area-scroll.md
@@ -0,0 +1,65 @@
+---
+title: Use contentInsetAdjustmentBehavior for Safe Areas
+impact: MEDIUM
+impactDescription: native safe area handling, no layout shifts
+tags: safe-area, scrollview, layout
+---
+
+## Use contentInsetAdjustmentBehavior for Safe Areas
+
+Use `contentInsetAdjustmentBehavior="automatic"` on the root ScrollView instead of wrapping content in SafeAreaView or manual padding. This lets iOS handle safe area insets natively with proper scroll behavior.
+
+**Incorrect (SafeAreaView wrapper):**
+
+```tsx
+import { SafeAreaView, ScrollView, View, Text } from 'react-native'
+
+function MyScreen() {
+ return (
+
+
+
+ Content
+
+
+
+ )
+}
+```
+
+**Incorrect (manual safe area padding):**
+
+```tsx
+import { ScrollView, View, Text } from 'react-native'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+
+function MyScreen() {
+ const insets = useSafeAreaInsets()
+
+ return (
+
+
+ Content
+
+
+ )
+}
+```
+
+**Correct (native content inset adjustment):**
+
+```tsx
+import { ScrollView, View, Text } from 'react-native'
+
+function MyScreen() {
+ return (
+
+
+ Content
+
+
+ )
+}
+```
+
+The native approach handles dynamic safe areas (keyboard, toolbars) and allows content to scroll behind the status bar naturally.
diff --git a/.github/skills/react-native-skills/rules/ui-scrollview-content-inset.md b/.github/skills/react-native-skills/rules/ui-scrollview-content-inset.md
new file mode 100644
index 0000000..bbebc3b
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-scrollview-content-inset.md
@@ -0,0 +1,45 @@
+---
+title: Use contentInset for Dynamic ScrollView Spacing
+impact: LOW
+impactDescription: smoother updates, no layout recalculation
+tags: scrollview, layout, contentInset, performance
+---
+
+## Use contentInset for Dynamic ScrollView Spacing
+
+When adding space to the top or bottom of a ScrollView that may change
+(keyboard, toolbars, dynamic content), use `contentInset` instead of padding.
+Changing `contentInset` doesn't trigger layout recalculation—it adjusts the
+scroll area without re-rendering content.
+
+**Incorrect (padding causes layout recalculation):**
+
+```tsx
+function Feed({ bottomOffset }: { bottomOffset: number }) {
+ return (
+
+ {children}
+
+ )
+}
+// Changing bottomOffset triggers full layout recalculation
+```
+
+**Correct (contentInset for dynamic spacing):**
+
+```tsx
+function Feed({ bottomOffset }: { bottomOffset: number }) {
+ return (
+
+ {children}
+
+ )
+}
+// Changing bottomOffset only adjusts scroll bounds
+```
+
+Use `scrollIndicatorInsets` alongside `contentInset` to keep the scroll
+indicator aligned. For static spacing that never changes, padding is fine.
diff --git a/.github/skills/react-native-skills/rules/ui-styling.md b/.github/skills/react-native-skills/rules/ui-styling.md
new file mode 100644
index 0000000..a3b9946
--- /dev/null
+++ b/.github/skills/react-native-skills/rules/ui-styling.md
@@ -0,0 +1,180 @@
+---
+title: Styling with NativeWind and Modern Patterns
+impact: MEDIUM
+impactDescription: consistent design, cleaner layouts, better DX
+tags: styling, nativewind, tailwind, css, layout, shadows
+---
+
+## Styling with NativeWind and Modern Patterns
+
+This project uses NativeWind (Tailwind CSS for React Native). Follow these
+patterns for consistent, maintainable styling.
+
+### NativeWind Basics
+
+Use Tailwind classes via the `className` prop:
+
+```tsx
+import { View, Text } from 'react-native'
+
+function Card({ title, children }: Props) {
+ return (
+
+ {title}
+ {children}
+
+ )
+}
+```
+
+### Prefer NativeWind Over StyleSheet
+
+**Incorrect (verbose StyleSheet):**
+
+```tsx
+import { View, Text, StyleSheet } from 'react-native'
+
+function Card({ title }: Props) {
+ return (
+
+ {title}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: '#fff',
+ borderRadius: 12,
+ padding: 16,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#111',
+ },
+})
+```
+
+**Correct (concise NativeWind):**
+
+```tsx
+import { View, Text } from 'react-native'
+
+function Card({ title }: Props) {
+ return (
+
+ {title}
+
+ )
+}
+```
+
+### Use gap for Spacing
+
+**Incorrect (margin on children):**
+
+```tsx
+
+ Title
+ Subtitle
+
+```
+
+**Correct (gap on parent):**
+
+```tsx
+
+ Title
+ Subtitle
+
+```
+
+### Combining Static and Dynamic Styles
+
+For conditional styling, combine NativeWind classes with template literals:
+
+```tsx
+function Button({ variant, disabled }: Props) {
+ return (
+
+
+ Press me
+
+
+ )
+}
+```
+
+### Platform-Specific Styles
+
+Use platform prefixes for platform-specific styles:
+
+```tsx
+
+ Content
+
+```
+
+### Dark Mode Support
+
+Use dark mode variants:
+
+```tsx
+
+ Content
+
+```
+
+### Modern React Native Style Properties
+
+When using inline styles (for animations or dynamic values), use modern patterns:
+
+**Use `borderCurve: 'continuous'` with borderRadius:**
+
+```tsx
+// Smoother iOS-style corners
+{ borderRadius: 12, borderCurve: 'continuous' }
+```
+
+**Use CSS boxShadow syntax:**
+
+```tsx
+// Modern shadow syntax
+{ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)' }
+```
+
+**Use experimental_backgroundImage for gradients:**
+
+```tsx
+// Native gradient support
+{
+ experimental_backgroundImage: 'linear-gradient(to bottom, #000, #fff)'
+}
+```
+
+### Common NativeWind Patterns
+
+**Flex layout:**
+```tsx
+
+```
+
+**Safe area padding:**
+```tsx
+
+```
+
+**Responsive spacing:**
+```tsx
+
+```
+
+Reference:
+
+- [NativeWind Documentation](https://www.nativewind.dev/)
+- [Tailwind CSS](https://tailwindcss.com/docs)