From fee636478fe775cb03e5ff2e87f546f4b89aff01 Mon Sep 17 00:00:00 2001 From: EQuimper Date: Fri, 13 Mar 2026 10:40:53 -0400 Subject: [PATCH] feat: add migration skill and update plugin configurations for react-native-ease --- .claude-plugin/marketplace.json | 21 ++ .claude-plugin/plugin.json | 5 + README.md | 131 ++++++----- package.json | 4 +- skills/refactor/SKILL.md | 399 ++++++++++++++++++++++++++++++++ 5 files changed, 501 insertions(+), 59 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 skills/refactor/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..e443520 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,21 @@ +{ + "name": "react-native-ease-plugins", + "owner": { + "name": "AppAndFlow", + "email": "devops@appandflow.com" + }, + "metadata": { + "description": "Claude Code skills for react-native-ease — migrate Reanimated/Animated code to react-native-ease" + }, + "plugins": [ + { + "name": "react-native-ease", + "source": "./", + "description": "Scan for Animated/Reanimated code and migrate to react-native-ease", + "version": "0.2.0", + "author": { + "name": "AppAndFlow" + } + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..edc680f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "react-native-ease", + "description": "Declarative native animations for React Native — migration tools", + "version": "0.2.0" +} diff --git a/README.md b/README.md index 208882e..99c16d3 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,24 @@ npm install react-native-ease yarn add react-native-ease ``` +## Migration Skill + +If you're already using `react-native-reanimated` or React Native's `Animated` API, this project includes an [Agent Skill](https://agentskills.io) that scans your codebase for animations that can be replaced with `react-native-ease` and migrates them automatically. + +```bash +npx skills add appandflow/react-native-ease +``` + +Then invoke the skill in your agent (e.g., `/refactor` in Claude Code). + +The skill will: + +1. Scan your project for Reanimated/Animated code +2. Classify which animations can be migrated (and which can't, with reasons) +3. Show a migration report with before/after details +4. Let you select which components to migrate +5. Apply the changes, preserving all non-animation logic + ## Quick Start ```tsx @@ -46,13 +64,13 @@ function FadeCard({ visible, children }) { ## When to use this vs Reanimated -| Use case | Ease | Reanimated | -|---|---|---| -| Fade/slide/scale on state change | ✅ | | -| Enter/exit animations | ✅ | | -| Gesture-driven animations (pan, pinch) | | ✅ | -| Layout animations (width, height) | | ✅ | -| Complex interpolations & chaining | | ✅ | +| Use case | Ease | Reanimated | +| -------------------------------------- | ---- | ---------- | +| Fade/slide/scale on state change | ✅ | | +| Enter/exit animations | ✅ | | +| Gesture-driven animations (pan, pinch) | | ✅ | +| Layout animations (width, height) | | ✅ | +| Complex interpolations & chaining | | ✅ | ## Guide @@ -67,11 +85,11 @@ Timing animations transition from one value to another over a fixed duration wit /> ``` -| Parameter | Type | Default | Description | -|---|---|---|---| -| `duration` | `number` | `300` | Duration in milliseconds | -| `easing` | `EasingType` | `'easeInOut'` | Easing curve (preset name or `[x1, y1, x2, y2]` cubic bezier) | -| `loop` | `string` | — | `'repeat'` restarts from the beginning, `'reverse'` alternates direction | +| Parameter | Type | Default | Description | +| ---------- | ------------ | ------------- | ------------------------------------------------------------------------ | +| `duration` | `number` | `300` | Duration in milliseconds | +| `easing` | `EasingType` | `'easeInOut'` | Easing curve (preset name or `[x1, y1, x2, y2]` cubic bezier) | +| `loop` | `string` | — | `'repeat'` restarts from the beginning, `'reverse'` alternates direction | Available easing curves: @@ -112,11 +130,11 @@ Spring animations use a physics-based model for natural-feeling motion. Great fo /> ``` -| Parameter | Type | Default | Description | -|---|---|---|---| -| `damping` | `number` | `15` | Friction — higher values reduce oscillation | -| `stiffness` | `number` | `120` | Spring constant — higher values mean faster animation | -| `mass` | `number` | `1` | Mass of the object — higher values mean slower, more momentum | +| Parameter | Type | Default | Description | +| ----------- | -------- | ------- | ------------------------------------------------------------- | +| `damping` | `number` | `15` | Friction — higher values reduce oscillation | +| `stiffness` | `number` | `120` | Spring constant — higher values mean faster animation | +| `mass` | `number` | `1` | Mass of the object — higher values mean slower, more momentum | Spring presets for common feels: @@ -189,16 +207,16 @@ All properties are set in the `animate` prop as flat values (no transform array) ```tsx @@ -280,11 +298,11 @@ By default, scale and rotation animate from the view's center. Use `transformOri /> ``` -| Value | Position | -|---|---| -| `{ x: 0, y: 0 }` | Top-left | +| Value | Position | +| -------------------- | ---------------- | +| `{ x: 0, y: 0 }` | Top-left | | `{ x: 0.5, y: 0.5 }` | Center (default) | -| `{ x: 1, y: 1 }` | Bottom-right | +| `{ x: 1, y: 1 }` | Bottom-right | ### Style Handling @@ -318,32 +336,32 @@ By default, scale and rotation animate from the view's center. Use `transformOri A `View` that animates property changes using native platform APIs. -| Prop | Type | Description | -|---|---|---| -| `animate` | `AnimateProps` | Target values for animated properties | -| `initialAnimate` | `AnimateProps` | Starting values for enter animations (animates to `animate` on mount) | -| `transition` | `Transition` | Animation configuration (timing, spring, or none) | -| `onTransitionEnd` | `(event) => void` | Called when all animations complete with `{ finished: boolean }` | -| `transformOrigin` | `{ x?: number; y?: number }` | Pivot point for scale/rotation as 0–1 fractions. Default: `{ x: 0.5, y: 0.5 }` (center) | -| `useHardwareLayer` | `boolean` | Android only — rasterize to GPU texture during animations. See [Hardware Layers](#hardware-layers-android). Default: `false` | -| `style` | `ViewStyle` | Non-animated styles (layout, colors, borders, etc.) | -| `children` | `ReactNode` | Child elements | -| ...rest | `ViewProps` | All other standard View props | +| Prop | Type | Description | +| ------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `animate` | `AnimateProps` | Target values for animated properties | +| `initialAnimate` | `AnimateProps` | Starting values for enter animations (animates to `animate` on mount) | +| `transition` | `Transition` | Animation configuration (timing, spring, or none) | +| `onTransitionEnd` | `(event) => void` | Called when all animations complete with `{ finished: boolean }` | +| `transformOrigin` | `{ x?: number; y?: number }` | Pivot point for scale/rotation as 0–1 fractions. Default: `{ x: 0.5, y: 0.5 }` (center) | +| `useHardwareLayer` | `boolean` | Android only — rasterize to GPU texture during animations. See [Hardware Layers](#hardware-layers-android). Default: `false` | +| `style` | `ViewStyle` | Non-animated styles (layout, colors, borders, etc.) | +| `children` | `ReactNode` | Child elements | +| ...rest | `ViewProps` | All other standard View props | ### `AnimateProps` -| Property | Type | Default | Description | -|---|---|---|---| -| `opacity` | `number` | `1` | View opacity (0–1) | -| `translateX` | `number` | `0` | Horizontal translation in pixels | -| `translateY` | `number` | `0` | Vertical translation in pixels | -| `scale` | `number` | `1` | Uniform scale factor (shorthand for `scaleX` + `scaleY`) | -| `scaleX` | `number` | `1` | Horizontal scale factor (overrides `scale` for X axis) | -| `scaleY` | `number` | `1` | Vertical scale factor (overrides `scale` for Y axis) | -| `rotate` | `number` | `0` | Z-axis rotation in degrees | -| `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) | -| `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) | -| `borderRadius` | `number` | `0` | Border radius in pixels (hardware-accelerated, clips children) | +| Property | Type | Default | Description | +| ----------------- | ------------ | --------------- | ------------------------------------------------------------------------------------ | +| `opacity` | `number` | `1` | View opacity (0–1) | +| `translateX` | `number` | `0` | Horizontal translation in pixels | +| `translateY` | `number` | `0` | Vertical translation in pixels | +| `scale` | `number` | `1` | Uniform scale factor (shorthand for `scaleX` + `scaleY`) | +| `scaleX` | `number` | `1` | Horizontal scale factor (overrides `scale` for X axis) | +| `scaleY` | `number` | `1` | Vertical scale factor (overrides `scale` for Y axis) | +| `rotate` | `number` | `0` | Z-axis rotation in degrees | +| `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) | +| `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) | +| `borderRadius` | `number` | `0` | Border radius in pixels (hardware-accelerated, clips children) | | `backgroundColor` | `ColorValue` | `'transparent'` | Background color (any RN color value). Timing-only on Android, spring+timing on iOS. | Properties not specified in `animate` default to their identity values. @@ -385,10 +403,7 @@ Applies values instantly with no animation. `onTransitionEnd` fires immediately Setting `useHardwareLayer` rasterizes the view into a GPU texture for the duration of the animation. This means animated property changes (opacity, scale, rotation) are composited on the RenderThread without redrawing the view hierarchy — useful for complex views with many children. ```tsx - + ``` **Trade-offs:** diff --git a/package.json b/package.json index 15ea73d..4d9b495 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "ios", "cpp", "*.podspec", + "skills", "!ios/build", "!android/build", "!android/gradle", @@ -28,7 +29,8 @@ "!**/__tests__", "!**/__fixtures__", "!**/__mocks__", - "!**/.*" + "!**/.*", + ".claude-plugin" ], "scripts": { "example": "yarn workspace react-native-ease-example", diff --git a/skills/refactor/SKILL.md b/skills/refactor/SKILL.md new file mode 100644 index 0000000..d0809fc --- /dev/null +++ b/skills/refactor/SKILL.md @@ -0,0 +1,399 @@ +--- +name: refactor +description: Scan for Animated/Reanimated code and migrate to EaseView +user-invocable: true +--- + +# react-native-ease refactor + +You are a migration assistant that converts `react-native-reanimated` and React Native's built-in `Animated` API code to `react-native-ease` `EaseView` components. + +Follow these 6 phases exactly. Do not skip phases or reorder them. + +--- + +## Phase 1: Discovery + +Scan the user's project for animation code: + +1. Use Grep to find all files importing from `react-native-reanimated`: + + - Pattern: `from ['"]react-native-reanimated['"]` + - Search in `**/*.{ts,tsx,js,jsx}` + +2. Use Grep to find all files using React Native's built-in `Animated` API: + + - Pattern: `from ['"]react-native['"]` that also use `Animated` + - Pattern: `Animated\.View|Animated\.Text|Animated\.Image|Animated\.Value|Animated\.timing|Animated\.spring` + +3. Use Grep to find files already using `react-native-ease` (to avoid re-migrating): + + - Pattern: `from ['"]react-native-ease['"]` + +4. Read each file that contains animation code. Build a list of components with their animation patterns. + +**Exclude** from scanning: + +- `node_modules/` +- `*.test.*` and `*.spec.*` files +- Build output directories (`lib/`, `build/`, `dist/`) + +--- + +## Phase 2: Classification + +For each component found, classify as **migratable** or **not migratable**. + +### Decision Tree + +Apply these checks in order. The first match determines the result: + +1. **Uses gesture APIs?** (`Gesture.Pan`, `Gesture.Pinch`, `Gesture.Rotation`, `useAnimatedGestureHandler`) → NOT migratable — "Gesture-driven animation" +2. **Uses scroll handler?** (`useAnimatedScrollHandler`, `onScroll` with `Animated.event`) → NOT migratable — "Scroll-driven animation" +3. **Uses shared element transitions?** (`sharedTransitionTag`) → NOT migratable — "Shared element transition" +4. **Uses `runOnUI` or worklet directives?** → NOT migratable — "Requires worklet runtime" +5. **Uses `withSequence` or `withDelay`?** → NOT migratable — "Animation sequencing not supported" +6. **Uses complex `interpolate()`?** (more than 2 input/output values) → NOT migratable — "Complex interpolation" +7. **Uses `layout={...}` prop?** → NOT migratable — "Layout animation" +8. **Animates unsupported properties?** (anything besides: opacity, translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY, borderRadius, backgroundColor) → NOT migratable — "Animates unsupported property: ``" +9. **Uses different transition configs per property?** (e.g., opacity uses 200ms timing, scale uses spring) → NOT migratable — "Per-property transition configs" +10. **Not driven by state?** (animation triggered by gesture/scroll value, not React state) → NOT migratable — "Not state-driven" +11. **Otherwise** → MIGRATABLE + +### Migratable Pattern Mapping + +Use this table to convert Reanimated/Animated patterns to EaseView: + +| Reanimated / Animated Pattern | EaseView Equivalent | +| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `useSharedValue` + `useAnimatedStyle` + `withTiming` for opacity, translate, scale, rotate, borderRadius, backgroundColor | `animate={{ prop: value }}` + `transition={{ type: 'timing', duration, easing }}` | +| `withSpring` | `transition={{ type: 'spring', damping, stiffness, mass }}` | +| `entering={FadeIn}` / `FadeIn.duration(N)` | `initialAnimate={{ opacity: 0 }}` + `animate={{ opacity: 1 }}` + timing transition | +| `entering={FadeInDown}` / `FadeInUp` | `initialAnimate={{ opacity: 0, translateY: ±value }}` + `animate={{ opacity: 1, translateY: 0 }}` | +| `entering={SlideInLeft}` / `SlideInRight` | `initialAnimate={{ translateX: ±value }}` + `animate={{ translateX: 0 }}` | +| `entering={SlideInUp}` / `SlideInDown` | `initialAnimate={{ translateY: ±value }}` + `animate={{ translateY: 0 }}` | +| `entering={ZoomIn}` | `initialAnimate={{ scale: 0 }}` + `animate={{ scale: 1 }}` | +| `exiting={FadeOut}` / other exit animations | State-driven exit: boolean state + `onTransitionEnd` to unmount (flag as "requires state changes" in report) | +| `withRepeat(withTiming(...), -1, false)` | `transition={{ type: 'timing', ..., loop: 'repeat' }}` + `initialAnimate` for start value | +| `withRepeat(withTiming(...), -1, true)` | `transition={{ type: 'timing', ..., loop: 'reverse' }}` + `initialAnimate` for start value | +| `Easing.linear` | `easing: 'linear'` | +| `Easing.ease` / `Easing.inOut(Easing.ease)` | `easing: 'easeInOut'` | +| `Easing.in(Easing.ease)` | `easing: 'easeIn'` | +| `Easing.out(Easing.ease)` | `easing: 'easeOut'` | +| `Easing.bezier(x1, y1, x2, y2)` | `easing: [x1, y1, x2, y2]` | +| `Animated.Value` + `Animated.timing` | Same `animate` + `transition` pattern — convert to state-driven | +| `Animated.Value` + `Animated.spring` | `animate` + `transition={{ type: 'spring' }}` — convert to state-driven | + +### Default Value Mapping + +**CRITICAL: Reanimated and EaseView have different defaults. You MUST explicitly set values to preserve the original animation behavior. Do not rely on EaseView defaults matching Reanimated defaults.** + +#### `withSpring` → EaseView spring + +| Parameter | Reanimated default | EaseView default | Action | +|---|---|---|---| +| `damping` | `10` | `15` | **Must set `damping: 10`** | +| `stiffness` | `100` | `120` | **Must set `stiffness: 100`** | +| `mass` | `1` | `1` | Same — omit | + +If the source code explicitly sets any of these values, carry them over as-is. If the source relies on Reanimated defaults (no explicit value), set the Reanimated default explicitly on the EaseView transition. + +Example — bare `withSpring(1)` with no config: +```typescript +// Before (Reanimated) +scale.value = withSpring(1); + +// After (EaseView) — must set damping: 10, stiffness: 100 to match +transition={{ type: 'spring', damping: 10, stiffness: 100 }} +``` + +**Note:** Reanimated v3+ uses duration-based spring by default (`duration: 550`, `dampingRatio: 1`) when no physics params are set. If migrating code that uses `withSpring` without any config, use `damping: 10, stiffness: 100` which matches the physics-based fallback. If the code explicitly sets `dampingRatio`/`duration`, convert using: `damping = dampingRatio * 2 * sqrt(stiffness * mass)`. + +#### `withTiming` → EaseView timing + +| Parameter | Reanimated default | EaseView default | Action | +|---|---|---|---| +| `duration` | `300` | `300` | Same — omit | +| `easing` | `Easing.inOut(Easing.quad)` | `'easeInOut'` (cubic) | **Must set `easing: [0.455, 0.03, 0.515, 0.955]`** | + +The easing curves are different! Reanimated's default is quadratic ease-in-out, EaseView's is cubic. Always set the easing explicitly when the source doesn't specify one. + +Example — bare `withTiming(1)` with no config: +```typescript +// Before (Reanimated) +opacity.value = withTiming(1); + +// After (EaseView) — must set quad easing to match +transition={{ type: 'timing', duration: 300, easing: [0.455, 0.03, 0.515, 0.955] }} +``` + +If the source explicitly sets an easing, map it using the easing table above. + +#### `Animated.timing` (old RN API) → EaseView timing + +| Parameter | RN Animated default | EaseView default | Action | +|---|---|---|---| +| `duration` | `500` | `300` | **Must set `duration: 500`** | +| `easing` | `Easing.inOut(Easing.ease)` | `'easeInOut'` | Same curve — omit | + +#### `Animated.spring` (old RN API) → EaseView spring + +RN Animated uses `friction`/`tension` by default: `friction: 7, tension: 40`. These map to: `stiffness = tension`, `damping = friction`. + +| Parameter | RN Animated default | EaseView default | Action | +|---|---|---|---| +| stiffness (tension) | `40` | `120` | **Must set `stiffness: 40`** | +| damping (friction) | `7` | `15` | **Must set `damping: 7`** | +| mass | `1` | `1` | Same — omit | + +### Unit Conversions + +- **Rotation:** Reanimated uses `'45deg'` strings in transforms → EaseView uses `45` (number, degrees). Strip the `'deg'` suffix and parse to number. +- **Translation:** Both use DIPs (density-independent pixels). No conversion needed. +- **Scale:** Both use unitless multipliers. No conversion needed. + +--- + +## Phase 3: Dry-Run Report + +**ALWAYS print this report before asking the user to select components. This report must be visible to the user before Phase 4.** + +Print a structured report. Do NOT apply any changes yet. + +Format: + +``` +## Migration Report + +### Summary +- Files scanned: X +- Components with animations: Y +- Migratable: Z | Not migratable: W + +### Migratable Components + +#### `path/to/file.tsx` — ComponentName +**Current:** Brief description of what the animation does and which API it uses +**Proposed:** What the EaseView equivalent looks like (include exact transition values with mapped defaults) +**Changes:** What will be added/removed/modified +**Note:** (only if applicable) "Requires state changes for exit animation" or other caveats + +### Not Migratable (will be skipped) + +#### `path/to/file.tsx` — ComponentName +**Reason:** Why it can't be migrated (from decision tree) +``` + +This report MUST be printed as text output in the conversation — not inside a plan, not collapsed. The user needs to read it before selecting components in Phase 4. + +--- + +## Phase 4: User Confirmation + +**CRITICAL: You MUST use the `AskUserQuestion` tool here. Do NOT use plan mode, do NOT use text prompts, do NOT ask inline. Call the `AskUserQuestion` tool directly.** + +Call `AskUserQuestion` with these exact parameters: +- `multiSelect`: `true` +- `questions`: a single question object with: + - `header`: `"Migrate"` + - `question`: `"Which components should be migrated to EaseView? All are selected — deselect any to skip."` + - `multiSelect`: `true` + - `options`: one entry per migratable component, each with: + - `label`: the component name (e.g., `"AnimatedButton"`) + - `description`: file path and brief animation description (e.g., `"src/components/animated-button.tsx — spring scale on press"`) + +Example tool call for 2 migratable components: + +```json +{ + "questions": [ + { + "header": "Migrate", + "question": "Which components should be migrated to EaseView? All are selected — deselect any to skip.", + "multiSelect": true, + "options": [ + { + "label": "AnimatedButton", + "description": "src/components/simple/animated-button.tsx — spring scale on press" + }, + { + "label": "Collapsible", + "description": "src/components/ui/collapsible.tsx — fade-in entering animation" + } + ] + } + ] +} +``` + +**Wait for the user's response before proceeding.** Do not enter plan mode. Do not apply any changes without the user selecting components. + +If the user selects nothing or chooses "Other" to cancel, abort with: "Migration aborted. No changes were made." + +Only proceed to Phase 5 with the components the user confirmed. + +--- + +## Phase 5: Apply Migrations + +For each confirmed component, apply the migration: + +### Migration Steps (per component) + +1. **Add EaseView import** if not already present: + + ```typescript + import { EaseView } from 'react-native-ease'; + ``` + +2. **Replace the animated view:** + + - `Animated.View` → `EaseView` + - `` → `` + +3. **Convert animation hooks to props:** + + - Remove `useSharedValue`, `useAnimatedStyle`, `withTiming`, `withSpring`, `withRepeat` calls + - Convert their values into `animate`, `initialAnimate`, and `transition` props + +4. **Convert entering/exiting animations:** + + - `entering={FadeIn}` → `initialAnimate={{ opacity: 0 }}` on the EaseView + `animate={{ opacity: 1 }}` + - For `exiting`: introduce a state variable and `onTransitionEnd` callback: + + ```typescript + const [visible, setVisible] = useState(true); + const [mounted, setMounted] = useState(true); + + // When triggering exit: + setVisible(false); + + // On the EaseView: + { + mounted && ( + { + if (finished && !visible) setMounted(false); + }} + > + ... + + ); + } + ``` + +5. **Clean up imports:** + + - Remove Reanimated imports that are no longer used in the file + - Keep any Reanimated imports still referenced by non-migrated code in the same file + - Never remove imports that are still used + +6. **Print progress:** + ``` + [1/N] Migrated ComponentName in path/to/file.tsx + ``` + +### Safety Rules + +These rules are non-negotiable. Violating them corrupts user code. + +1. **When in doubt, skip.** If a pattern is ambiguous or you're not confident in the migration, add it to "Not Migratable" with reason: "Complex pattern — manual review recommended" +2. **Never remove imports still used elsewhere in the file.** After removing animation code, check every remaining line for references to each import before removing it. +3. **Preserve all non-animation logic.** Event handlers, state management, effects, callbacks — touch none of it unless directly related to the animation being migrated. +4. **Preserve component structure and public API.** Props, ref forwarding, exported types — keep them identical. +5. **Handle mixed files correctly.** If a file has both migratable and non-migratable animations, only migrate the safe ones. Keep Reanimated imports if any Reanimated code remains. +6. **Map rotation units correctly.** Reanimated `'45deg'` string → EaseView `45` number. If the source uses radians, convert: `radians * (180 / Math.PI)`. +7. **Map easing presets correctly.** See the mapping table in Phase 2. +8. **Do not introduce TypeScript errors.** Ensure all types are correct after migration. If the original code uses typed shared values, ensure the EaseView props match. + +--- + +## Phase 6: Final Report + +After all migrations are applied, print: + +``` +## Migration Complete + +### Changed (X components) +- `path/to/file.tsx` — ComponentName: brief description of what was migrated + +### Unchanged (Y components) +- `path/to/file.tsx` — ComponentName: reason skipped + +### Next Steps +- Run your app and verify animations visually +- Run your test suite to check for regressions +- If no Reanimated code remains, consider removing `react-native-reanimated` from dependencies +``` + +--- + +## EaseView API Reference (for migration accuracy) + +### Supported Animatable Properties + +All properties in the `animate` prop: + +| Property | Type | Default | Notes | +| ----------------- | ------------ | --------------- | ------------------------------------ | +| `opacity` | `number` | `1` | 0–1 range | +| `translateX` | `number` | `0` | In DIPs (density-independent pixels) | +| `translateY` | `number` | `0` | In DIPs | +| `scale` | `number` | `1` | Shorthand for scaleX + scaleY | +| `scaleX` | `number` | `1` | Overrides scale for X axis | +| `scaleY` | `number` | `1` | Overrides scale for Y axis | +| `rotate` | `number` | `0` | Z-axis rotation in degrees | +| `rotateX` | `number` | `0` | X-axis rotation in degrees (3D) | +| `rotateY` | `number` | `0` | Y-axis rotation in degrees (3D) | +| `borderRadius` | `number` | `0` | In pixels | +| `backgroundColor` | `ColorValue` | `'transparent'` | Any RN color value | + +### Transition Types + +**Timing:** + +```typescript +transition={{ + type: 'timing', + duration: 300, // ms, default 300 + easing: 'easeInOut', // 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | [x1,y1,x2,y2] + loop: 'repeat', // 'repeat' | 'reverse' — requires initialAnimate +}} +``` + +**Spring:** + +```typescript +transition={{ + type: 'spring', + damping: 15, // default 15 + stiffness: 120, // default 120 + mass: 1, // default 1 +}} +``` + +**None (instant):** + +```typescript +transition={{ type: 'none' }} +``` + +### Key Props + +- `animate` — target values for animated properties +- `initialAnimate` — starting values (animates to `animate` on mount) +- `transition` — animation config (timing or spring) +- `onTransitionEnd` — callback with `{ finished: boolean }` +- `transformOrigin` — pivot point as `{ x: 0-1, y: 0-1 }`, default center +- `useHardwareLayer` — Android GPU optimization (boolean, default false) + +### Important Constraints + +- **Loop requires timing** (not spring) and `initialAnimate` must define the start value +- **No per-property transitions** — one transition config applies to all animated properties +- **No animation sequencing** — no equivalent to `withSequence`/`withDelay` +- **No gesture/scroll-driven animations** — EaseView is state-driven only +- **Style/animate conflict** — if a property appears in both `style` and `animate`, the animated value wins