
Native Lottie
- 1 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
native-lottie is a skill for integrating lottie-react-native and dotLottie native animations, including asset bundling, playback refs, and reduced motion.
About
This skill integrates Lottie animations into Expo and React Native apps. Developers use it for lottie-react-native and dotLottie assets, asset bundling, playback refs, and unmount cleanup. It also covers accessibility labels, reduced-motion behavior, and iOS/Android validation, with an audit CLI to surface issues.
- Guidance for lottie-react-native and dotLottie native assets in Expo
- Covers asset bundling, playback refs, accessibility, and reduced motion
- Includes an audit CLI and platform validation checklist
Native Lottie by the numbers
- 1 all-time installs (skills.sh)
- Ranked #959 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
native-lottie capabilities & compatibility
- Capabilities
- frontend · ui design
- Use cases
- frontend · ui design
What native-lottie says it does
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Large JSON animations can hurt startup and memory.
Native asset paths differ from web URLs and need bundler-safe imports.
npx skills add https://github.com/bjornmelin/dev-skills --skill native-lottieAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Bundling and controlling lottie-react-native or dotLottie animations in an Expo/React Native app with reduced-motion behavior.
Who is it for?
Developers adding lottie-react-native or dotLottie animations to Expo/React Native apps.
Skip if: Browser Lottie, interactive Rive state machines, or code-driven Reanimated motion.
When should I use this skill?
Working with lottie-react-native, dotLottie, LottieView, or .lottie native assets.
By the numbers
- Ships a scan/doctor audit CLI (scripts/audit.mjs)
- Bundles a starter LottieView example fixture
Files
Native Lottie
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Operating Contract
Use this skill as a compact router plus domain checklist. Load references only when the current task matches their condition. Do not cite local scrape paths, machine cache paths, or hidden source locations. Verify API details against the target repo's installed package versions before editing.
Source Order
1. Inspect the target repo's installed packages, framework/runtime versions, local design tokens, accessibility policy, and existing motion patterns. 2. Use the bundled references below for skill-specific gotchas and copied source excerpts. 3. Use official current docs/package source as API truth when local code or bundled notes are version-sensitive.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
<!-- skill-resources:start -->
Bundled Resources
references/native-lottie-asset-lifecycle.md- Native Lottie asset lifecycle. Read for LottieView refs, source formats, asset imports, and playback control.references/dotlottie-native-boundaries.md- dotLottie native boundaries. Read when using .lottie assets or LottieFiles native packages.references/accessibility-performance.md- Native Lottie accessibility and performance. Read for labels, reduced motion, looping, asset size, and platform proof.references/designer-handoff-and-feature-support.md- Native Lottie designer handoff and feature support. Read when a Lottie asset is new, visually wrong on device, large, or different from the design preview.references/native-playback-control-refs.md- Native Lottie playback refs and lifecycle. Read when code controls LottieView refs, imperative playback, progress, segments, or component unmount.references/docs-lottie-react-native-readme.md- Copied source excerpt. Load only when exact upstream wording or API detail is needed.references/index.md- Complete reference inventory and routing summary.references/source-ledger.md- Source list, checked date, and copy policy.references/provenance.json- Machine-readable source and local-resource metadata.scripts/audit.mjs- Self-contained audit CLI; rundoctorbeforescanwhen setup is unclear.assets/templates/native-lottie-audit-report.md- Audit response/report template.assets/templates/native-lottie-review-checklist.md- Manual review checklist.assets/examples/native-lottie-starter.tsx- Starter fixture/example for this skill.evals/trigger-queries.json- Trigger/near-miss eval set for description tuning.evals/evals.json- Task-quality evals with assertions.
<!-- skill-resources:end -->
Audit CLI
node scripts/audit.mjs doctor --root . --format json
node scripts/audit.mjs scan --root . --format markdown
node scripts/audit.mjs scan --root . --format json --output native-lottie-audit.jsonTreat script findings as leads. Verify every finding against current code before changing behavior or reporting it as valid.
Closeout
Before finalizing, run the repo's focused validation command, this skill's audit CLI when relevant, and any browser/device/manual proof required by the changed surface. Report commands run, findings fixed, findings skipped with reasons, and residual risk.
interface:
display_name: "Native Lottie"
short_description: "React Native Lottie asset integration"
default_prompt: "Use $native-lottie for lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation."
policy:
allow_implicit_invocation: true
import { useEffect, useState } from 'react';
import { AccessibilityInfo } from 'react-native';
import LottieView from 'lottie-react-native';
/**
* Renders a success animation that autoplays only when reduced motion is off and never loops.
*
* @returns A Lottie success animation component.
*/
export function SuccessAnimation() {
const [isReduceMotion, setIsReduceMotion] = useState(true);
useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled()
.then((enabled) => {
if (mounted) setIsReduceMotion(enabled);
})
.catch(() => {
if (mounted) setIsReduceMotion(true);
});
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
setIsReduceMotion,
);
return () => {
mounted = false;
subscription.remove();
};
}, []);
return (
<LottieView
source={require('./success.json')}
autoPlay={!isReduceMotion}
loop={false}
accessibilityLabel="Success"
/>
);
}
{
"v": "5.7.4",
"fr": 30,
"ip": 0,
"op": 60,
"w": 120,
"h": 120,
"nm": "success",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "check",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100 },
"r": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [60, 60, 0] },
"a": { "a": 0, "k": [0, 0, 0] },
"s": { "a": 0, "k": [100, 100, 100] }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "sh",
"ks": {
"a": 0,
"k": {
"i": [[0, 0], [0, 0], [0, 0]],
"o": [[0, 0], [0, 0], [0, 0]],
"v": [[-28, 0], [-8, 20], [32, -24]],
"c": false
}
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.13, 0.62, 0.33, 1] },
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 10 },
"lc": 2,
"lj": 2
},
{ "ty": "tr", "p": { "a": 0, "k": [0, 0] }, "a": { "a": 0, "k": [0, 0] }, "s": { "a": 0, "k": [100, 100] }, "r": { "a": 0, "k": 0 }, "o": { "a": 0, "k": 100 } }
]
}
],
"ip": 0,
"op": 60,
"st": 0,
"bm": 0
}
]
}
Native Lottie Audit Report
Scope
- Target repo/root:
- Skill: native-lottie
- Audit command:
- Date:
- Reviewer:
- Installed packages/versions:
- Runtime/platforms checked:
Summary
- High:
- Medium:
- Low:
- Overall status:
Findings
[severity] [ruleId] Short Title
- File:
- Line:
- Confidence:
- Evidence:
- Why it matters for Lottie:
- Recommendation:
- Validation required:
- Status:
Validation
- Commands run:
- Source/package versions checked:
- Runtime/browser/device proof:
- Accessibility/reduced-motion proof:
- Skipped checks and reason:
Residual Risk
- Accepted tradeoffs:
- Follow-up owner:
Native Lottie Review Checklist
Before Editing
- [ ] Confirm this skill owns the task and adjacent skills do not.
- [ ] Inspect installed package/framework/runtime versions.
- [ ] Read the first matching reference from SKILL.md resource routing.
- [ ] Run
node scripts/audit.mjs doctor --root <repo> --format jsonwhen setup is unclear.
Manual Review
- [ ] Check Expo SDK package compatibility and asset format.
- [ ] Bundle assets through the app asset pipeline.
- [ ] Own playback refs, pause/stop behavior, and unmount cleanup.
- [ ] Validate Android/iOS rendering, reduced motion, and accessibility labels.
- [ ] Check gotcha: Large JSON animations can hurt startup and memory.
- [ ] Check gotcha: Autoplay loops need pause/reduced-motion behavior.
- [ ] Check gotcha: Native asset paths differ from web URLs and need bundler-safe imports.
- [ ] Verify reduced-motion or accessible static fallback when motion is nonessential.
- [ ] Verify cleanup on unmount/navigation or owner teardown.
Closeout
- [ ] Run
node scripts/audit.mjs scan --root <repo> --format markdownwhen auditing. - [ ] Run the target repo's focused validation commands.
- [ ] Capture browser/device/manual proof when the changed surface renders motion.
- [ ] Report fixed findings, skipped findings with reasons, and residual risk.
{
"evals": [
{
"id": "native-lottie-task-1",
"prompt": "In src/components/Hero.tsx, implement lottie-react-native behavior for a product surface and explain the validation gates.",
"expected_output": "Uses native-lottie routing, checks installed versions, applies domain-specific Lottie guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for Lottie.",
"Uses at least one native-lottie bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-lottie-task-2",
"prompt": "Review this branch for Lottie motion issues: package versions look current but users report jank and missing reduced-motion behavior.",
"expected_output": "Uses native-lottie routing, checks installed versions, applies domain-specific Lottie guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for Lottie.",
"Uses at least one native-lottie bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-lottie-task-3",
"prompt": "Create a minimal example using dotLottie React Native that includes cleanup and accessibility notes.",
"expected_output": "Uses native-lottie routing, checks installed versions, applies domain-specific Lottie guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for Lottie.",
"Uses at least one native-lottie bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.",
"Does not route the task to an unrelated near-miss skill."
]
},
{
"id": "native-lottie-task-4",
"prompt": "Migrate an existing motion implementation to the right Lottie primitive without changing unrelated UI.",
"expected_output": "Uses native-lottie routing, checks installed versions, applies domain-specific Lottie guidance, runs or recommends the bundled audit CLI where relevant, and reports validation evidence.",
"assertions": [
"Mentions the relevant installed package/runtime/version checks for Lottie.",
"Uses at least one native-lottie bundled reference or explains why no extra reference was needed.",
"Includes reduced-motion/accessibility or explains why the effect is essential and already covered.",
"Includes cleanup/interruption/runtime validation appropriate to lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.",
"Does not route the task to an unrelated near-miss skill."
]
}
]
}
{
"queries": [
{
"query": "Can you help with lottie-react-native in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "lottie-react-native is directly in scope for native-lottie."
},
{
"query": "Can you help with dotLottie React Native in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "dotLottie React Native is directly in scope for native-lottie."
},
{
"query": "Can you help with LottieView in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "LottieView is directly in scope for native-lottie."
},
{
"query": "Can you help with .lottie native in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": ".lottie native is directly in scope for native-lottie."
},
{
"query": "Can you help with After Effects native animation in our app and make sure it has cleanup, reduced-motion behavior, and validation?",
"should_trigger": true,
"reason": "After Effects native animation is directly in scope for native-lottie."
},
{
"query": "Review this Lottie implementation for lottie-react-native and dotlottie native assets and produce an audit report.",
"should_trigger": true,
"reason": "The task asks for Lottie motion expertise owned by native-lottie."
},
{
"query": "Audit this lottie-react-native player for refs, cleanup, reduced motion, and asset bundling.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-lottie."
},
{
"query": "Review these dotLottie native assets for Metro support, test mocks, and platform validation.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-lottie."
},
{
"query": "Check this After Effects handoff for unsupported Lottie features and native playback controls.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-lottie."
},
{
"query": "Validate this Lottie success animation for accessibility labels and paused/static fallback behavior.",
"should_trigger": true,
"reason": "Distinct in-scope trigger wording for native-lottie."
},
{
"query": "make this API route return paginated JSON from Postgres",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "fix this Zod schema for a nested form payload",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "write a SQL migration for an orders table",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "debug why Clerk middleware redirects on localhost",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "convert this CSV file into YAML",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "add Open Graph metadata to this marketing page",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "optimize a server function cold start without changing UI animation",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "write a unit test for a pure date formatter",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "create a Dockerfile for this Python service",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
},
{
"query": "review this payment webhook signature verification",
"should_trigger": false,
"reason": "Near miss or unrelated task for native-lottie."
}
]
}
Native Lottie accessibility and performance
Skill: native-lottie Checked at: 2026-06-04
When To Load
- Read for labels, reduced motion, looping, asset size, and platform proof.
Operating Guidance
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow Details
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native Lottie designer handoff and feature support
Skill: native-lottie Checked at: 2026-06-04
When To Load
- Read when a Lottie asset is new, visually wrong on device, large, or different from the design preview.
Source Anchors
- https://docs.expo.dev/versions/latest/sdk/lottie/
- https://github.com/airbnb/lottie-web/wiki/Usage
Reference Notes
- Review designer export settings, frame rate, dimensions, image/font dependencies, masks, mattes, expressions, and unsupported runtime features before blaming code.
- Native renderers can diverge from web previews. Device proof on iOS and Android is part of asset acceptance when visual fidelity matters.
- Large JSON assets affect app bundle size, parse time, and memory.
Focused Checks
- Check asset size, external files, playback speed, loop mode, and platform rendering.
- Verify reduced-motion behavior for autoplay and loops.
Failure Modes
- Using remote JSON assets in native without cache/security policy.
- Accepting a web-only Lottie preview as native proof.
Operating Guidance
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow Details
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native Lottie Reference
Use this file when a task needs concrete Expo or React Native Lottie details. Check installed package versions before recommending exact code or versions in a target repo.
Current Package Facts
- Expo SDK 56 targets React Native 0.85 and React 19.2.3. Its bundled native
module list pins lottie-react-native to ~7.3.4.
lottie-react-nativelatest npm release observed during refresh:
7.3.8. Expo SDK 56 apps should normally keep the Expo-compatible install unless a tested override is intentional.
@lottiefiles/dotlottie-react-nativelatest npm release observed during
refresh: 0.9.3. The published package depends on @lottiefiles/dotlottie-react ^0.19.4 for its web path.
- Expo recommends
expo installfor third-party React Native libraries when it
can select compatible versions, and development builds for native libraries that Expo Go does not include.
lottie-react-native
Use this player for standard Bodymovin JSON and Expo-friendly playback.
Useful API surface from the current source/README:
- default import:
LottieView; - imperative commands:
play(startFrame?, endFrame?),reset(),pause(),
resume();
- common props:
source,progress,speed,duration,loop,autoPlay,
resizeMode, renderMode, cacheComposition, colorFilters, imageAssetsFolder, platform text filters, and animation lifecycle events;
progressis normalized0..1and should have a single owner;colorFilterstarget layer keypaths and need iOS/Android export proof.
Local JSON Playback
Prefer static local assets for production reliability:
import LottieView from 'lottie-react-native';
export function SuccessMark() {
return (
<LottieView
source={require('../assets/success.json')}
style={{ width: 96, height: 96 }}
resizeMode="contain"
loop={false}
autoPlay={false}
/>
);
}Imperative Playback
Own the ref in the wrapper that owns the lifecycle:
import { useEffect, useRef } from 'react';
import LottieView from 'lottie-react-native';
export function OnceOnMount() {
const animationRef = useRef<LottieView>(null);
useEffect(() => {
animationRef.current?.play();
return () => animationRef.current?.reset();
}, []);
return (
<LottieView
ref={animationRef}
source={require('../assets/complete.json')}
style={{ width: 120, height: 120 }}
loop={false}
/>
);
}Progress Control
Use progress when app state owns the visual state. React Native Animated progress examples use useNativeDriver: false.
import { useEffect, useRef } from 'react';
import { Animated, Easing } from 'react-native';
import LottieView from 'lottie-react-native';
const AnimatedLottieView = Animated.createAnimatedComponent(LottieView);
export function ControlledProgress() {
const progress = useRef(new Animated.Value(0));
useEffect(() => {
const animation = Animated.timing(progress.current, {
toValue: 1,
duration: 500,
easing: Easing.linear,
useNativeDriver: false,
});
animation.start();
return () => animation.stop();
}, []);
return (
<AnimatedLottieView
source={require('../assets/progress.json')}
progress={progress.current}
style={{ width: 120, height: 120 }}
/>
);
}.lottie Files
Expo Asset lists .lottie as an embeddable media type, but custom imports still need Metro asset extension support.
For lottie-react-native, add lottie to Metro asset extensions before importing packaged files:
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
module.exports = mergeConfig(defaultConfig, {
resolver: {
assetExts: [...defaultConfig.resolver.assetExts, 'lottie'],
},
});For Jest or Vitest, add a stable file stub when tests import .lottie:
// __mocks__/lottieMock.js
module.exports = 'lottie-test-file-stub';module.exports = {
moduleNameMapper: {
'\\.(lottie)$': '<rootDir>/__mocks__/lottieMock.js',
},
};dotLottie React Native
Use @lottiefiles/dotlottie-react-native only when the dotLottie runtime is part of the product contract. It is not just a drop-in replacement for ordinary JSON playback.
Current 0.9.3 API facts from the published package:
- source import:
DotLottie; - ref type:
Dotlottie; source: local module number, string, or{ uri };- props include
loop,autoplay,speed,themeId,marker,segment,
playMode, useFrameInterpolation, stateMachineId, renderer, and lifecycle/state-machine callbacks;
- methods include
play,pause,stop,setLoop,setSpeed,
setPlayMode, setFrame, freeze, unfreeze, resize, setSegment, setMarker, setTheme, loadAnimation, state-machine input methods, and metrics such as totalFrames, duration, currentFrame, isPlaying, activeThemeId, and activeAnimationId.
Expo apps need native binaries for this package. The package ships a config plugin and docs describe prebuild/development-build/EAS paths; Expo Go is not a valid native runtime proof.
import { useRef } from 'react';
import { Button, View } from 'react-native';
import { DotLottie, Mode, type Dotlottie } from '@lottiefiles/dotlottie-react-native';
export function DotLottieBadge() {
const ref = useRef<Dotlottie>(null);
return (
<View>
<DotLottie
ref={ref}
source={require('../assets/badge.lottie')}
style={{ width: 160, height: 160 }}
loop={false}
autoplay={false}
playMode={Mode.FORWARD}
/>
<Button title="Play" onPress={() => ref.current?.play()} />
</View>
);
}Asset Lifecycle Checklist
- Confirm file format, size, dimensions, frame rate, frame range, markers,
animation IDs, theme IDs, state-machine IDs, text layers, keypaths, and external images before coding.
- Check the Lottie supported-features matrix for masks, mattes, merge paths,
expressions, text, effects, gradients, shadows, and platform differences.
- For image-backed animations, verify Android
imageAssetsFolderor native
asset placement and rebuild after asset changes.
- Centralize contract strings: keypaths, text filter targets, markers,
segments, animation IDs, theme IDs, and state-machine inputs.
- Define missing/corrupt asset behavior and static poster/final-frame fallback.
- Prefer local versioned assets. Remote Lottie requires loading, cache,
integrity, offline, privacy, and failure behavior.
- Validate OTA/static asset inclusion when using Expo Updates or static export.
Reduced Motion
Use an existing app hook if one exists. Otherwise, React Native AccessibilityInfo.isReduceMotionEnabled() gives the initial state and reduceMotionChanged lets long-lived components react to setting changes.
import { AccessibilityInfo } from 'react-native';
import { useEffect, useState } from 'react';
export function useReduceMotion() {
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
if (mounted) setReduceMotion(enabled);
});
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
setReduceMotion,
);
return () => {
mounted = false;
subscription.remove();
};
}, []);
return reduceMotion;
}The useReduceMotion hook centralizes system preference reads and subscription cleanup so playback components can disable decorative autoplay without duplicating accessibility wiring.
When reduced motion is enabled:
- do not autoplay decorative animations;
- replace indefinite loops with a static poster, final frame, or concise state
UI;
- keep essential state changes visible without relying on completion callbacks;
- preserve accessible labels/state text separately from the animation.
Performance And Reliability
- Stable dimensions prevent layout shifts and blank first frames.
- Avoid large remote JSON on first paint and avoid many active Lottie instances
in virtualized rows.
- Android
renderMode, safe mode, hardware acceleration, and composition cache
settings should be changed only after reproducing a platform issue.
- Do not use Lottie as a splash-screen bridge for slow startup without a native
splash fallback.
- Add
onAnimationFailure/onLoadErrorhandling when asset availability is
not guaranteed.
Validation Closeout
Run the target repo's focused gates plus manual runtime checks for:
- install/version alignment (
expo install --checkor equivalent); - typecheck and focused tests;
- Metro bundling and Jest/Vitest
.lottiemock when used; - iOS playback, final frame, and missing/corrupt asset path;
- Android playback, final frame, and missing/corrupt asset path;
- reduced-motion fallback;
- route unmount/interruption and replay behavior;
- progress, color filter, marker, segment, theme, animation ID, or
state-machine contract behavior;
- native rebuild proof after package, native image, config plugin, prebuild, or
dotLottie runtime changes.
dotLottie native boundaries
Skill: native-lottie Checked at: 2026-06-04
When To Load
- Read when using .lottie assets or LottieFiles native packages.
Operating Guidance
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow Details
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native Lottie Reference Index
Read only the file that matches the task. Keep SKILL.md as the router.
Tailored References
native-lottie-asset-lifecycle.md- Native Lottie asset lifecycle.dotlottie-native-boundaries.md- dotLottie native boundaries.accessibility-performance.md- Native Lottie accessibility and performance.designer-handoff-and-feature-support.md- Native Lottie designer handoff and feature support.native-playback-control-refs.md- Native Lottie playback refs and lifecycle.
Copied Source Excerpts
docs-lottie-react-native-readme.md
Native Lottie asset lifecycle
Skill: native-lottie Checked at: 2026-06-04
When To Load
- Read for LottieView refs, source formats, asset imports, and playback control.
Operating Guidance
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow Details
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
Native Lottie playback refs and lifecycle
Skill: native-lottie Checked at: 2026-06-04
When To Load
- Read when code controls LottieView refs, imperative playback, progress, segments, or component unmount.
Source Anchors
- https://docs.expo.dev/versions/latest/sdk/lottie/
- https://reactnative.dev/docs/accessibility
Reference Notes
- The owning component should control playback refs and stop/reset behavior. Avoid globally reachable animation handles.
- Progress-driven Lottie should have a stable source of truth and avoid per-frame React re-renders where native animation values can own the update.
- Autoplay/loop should pause or simplify under reduced motion and when screens lose focus.
Focused Checks
- Test screen blur/focus, unmount, app background/foreground, and asset replacement.
- Check accessible label/state outside the animation view.
Failure Modes
- Leaving looping animations active behind a hidden route.
- Using decorative animation progress as the only indication of completion.
Operating Guidance
lottie-react-native and dotLottie native assets, Expo compatibility, asset bundling, refs, playback control, accessibility, reduced motion, and platform validation.
Decision Boundaries
- Use web-lottie for browser Lottie.
- Use native-rive for interactive Rive state machines.
- Use native-motion-core for code-driven Reanimated motion.
Workflow Details
1. Check Expo SDK package compatibility and asset format. 2. Bundle assets through the app asset pipeline. 3. Own playback refs, pause/stop behavior, and unmount cleanup. 4. Validate Android/iOS rendering, reduced motion, and accessibility labels.
Gotchas
- Large JSON animations can hurt startup and memory.
- Autoplay loops need pause/reduced-motion behavior.
- Native asset paths differ from web URLs and need bundler-safe imports.
Validation Notes
- Inspect installed package versions and local architecture before applying examples.
- Prefer the bundled
scripts/audit.mjs doctor --root <repo> --format jsoncommand when setup is unclear. - Use
scripts/audit.mjs scan --root <repo> --format markdownfor repeatable static findings, then manually verify every finding against current code. - Close with repo-specific checks and user-visible runtime proof when this skill affects a rendered surface.
{
"skill": "native-lottie",
"checked_at": "2026-06-04",
"copy_policy": "Bundled references are tailored copied excerpts or derived notes from official docs/package sources where license permits. Verify installed package versions before applying API examples.",
"upstream_sources": [
{
"label": "lottie-web, lottie-react-native, dotLottie web/native package docs and source metadata."
},
{
"label": "Expo SDK docs for native package compatibility when applicable."
},
{
"label": "Agent Skills specification and best practices."
}
],
"tailored_reference_files": [
{
"file": "references/native-lottie-asset-lifecycle.md",
"title": "Native Lottie asset lifecycle",
"sources": []
},
{
"file": "references/dotlottie-native-boundaries.md",
"title": "dotLottie native boundaries",
"sources": []
},
{
"file": "references/accessibility-performance.md",
"title": "Native Lottie accessibility and performance",
"sources": []
},
{
"file": "references/designer-handoff-and-feature-support.md",
"title": "Native Lottie designer handoff and feature support",
"sources": [
"https://docs.expo.dev/versions/latest/sdk/lottie/",
"https://github.com/airbnb/lottie-web/wiki/Usage"
]
},
{
"file": "references/native-playback-control-refs.md",
"title": "Native Lottie playback refs and lifecycle",
"sources": [
"https://docs.expo.dev/versions/latest/sdk/lottie/",
"https://reactnative.dev/docs/accessibility"
]
}
],
"bundled_source_excerpt_files": [
"references/docs-lottie-react-native-readme.md"
],
"local_files": [
"SKILL.md",
"scripts/audit.mjs",
"assets/templates",
"assets/examples",
"evals/evals.json",
"evals/trigger-queries.json"
]
}
native-lottie Source Ledger
Checked at: 2026-06-04
This ledger is skill-local and portable. It names the upstream sources used for the bundled guidance, copied excerpts, generated evals, examples, and audit rules. It intentionally does not reference local scrape paths or machine cache locations.
Primary Sources
- lottie-web, lottie-react-native, dotLottie web/native package docs and source metadata.
- Expo SDK docs for native package compatibility when applicable.
- Agent Skills specification and best practices.
Bundled Source Excerpts
references/docs-lottie-react-native-readme.md
Tailored Reference Files
references/native-lottie-asset-lifecycle.md- Native Lottie asset lifecyclereferences/dotlottie-native-boundaries.md- dotLottie native boundariesreferences/accessibility-performance.md- Native Lottie accessibility and performancereferences/designer-handoff-and-feature-support.md- Native Lottie designer handoff and feature supportreferences/native-playback-control-refs.md- Native Lottie playback refs and lifecycle
Local Additions
references/provenance.jsonrecords source URLs, package facts, and copy policy.scripts/audit.mjsprovides the repeatable static audit CLI for this skill.assets/templates/contains output templates and review checklists.assets/examples/contains small starter examples or fixtures.evals/contains trigger and task-quality evals.
Use official docs and installed package versions as API truth when they conflict with older bundled notes.
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const profile = {
"skillName": "native-lottie",
"rules": [
{
"id": "lottie.remote-asset",
"severity": "medium",
"confidence": "medium",
"category": "asset",
"pattern": "(?:path|src|source|uri|url)\\s*[:=]\\s*{?\\s*['\"]https?://[^'\"]*(?:lottie|\\.json|\\.lottie)",
"message": "A Lottie asset appears to load from a remote URL.",
"recommendation": "Prefer local versioned assets or document loading, cache, offline, privacy, and failure behavior for the remote dependency."
},
{
"id": "lottie.loop-autoplay-no-reduced-motion",
"severity": "medium",
"confidence": "medium",
"category": "accessibility",
"kind": "fileContainsBothWithout",
"include": "LottieView|DotLottie|lottie-react-native|dotlottie-react-native",
"also": "\\b(?:loop|autoPlay|autoplay)\\s*(?:=|:)\\s*{?\\s*true\\b|<[^>]*\\s(?:loop|autoPlay|autoplay)(?!\\s*=)(?=\\s|>|/)",
"without": "reduced|ReduceMotion|useReducedMotion|motion-reduce|AccessibilityInfo|poster|fallback|finalFrame|static",
"message": "Looping/autoplay Lottie usage lacks an obvious reduced-motion or poster fallback.",
"recommendation": "Pause, replace, or simplify decorative asset playback under reduced motion."
},
{
"id": "lottie.imperative-no-unmount-cleanup",
"severity": "medium",
"confidence": "medium",
"category": "lifecycle",
"kind": "fileContainsBothWithout",
"include": "LottieView|DotLottie|lottie-react-native|dotlottie-react-native",
"also": "\\.current\\?\\.(?:play|resume)\\(",
"without": "return\\s*\\(?.*(?:reset|pause|stop|freeze|animation\\.stop|subscription\\.remove)|\\.current\\?\\.(?:reset|pause|stop|freeze)\\(",
"message": "Imperative Lottie playback lacks obvious unmount/interruption cleanup.",
"recommendation": "Verify the owning component resets, pauses, stops, freezes, or stops Animated progress when it unmounts or is interrupted."
},
{
"id": "lottie.progress-mixed-control",
"severity": "medium",
"confidence": "medium",
"category": "lifecycle",
"kind": "fileContainsBoth",
"include": "progress\\s*=",
"also": "\\.current\\?\\.(?:play|pause|reset|resume)\\(",
"message": "A Lottie component appears to mix controlled progress and imperative playback.",
"recommendation": "Use one playback owner per component: progress, imperative ref, marker/segment, state machine, or declarative autoplay."
},
{
"id": "lottie.progress-native-driver",
"severity": "medium",
"confidence": "medium",
"category": "lifecycle",
"pattern": "progress[\\s\\S]{0,2000}Animated\\.timing[\\s\\S]{0,800}useNativeDriver\\s*:\\s*true|Animated\\.timing[\\s\\S]{0,800}useNativeDriver\\s*:\\s*true[\\s\\S]{0,2000}progress",
"message": "Lottie progress appears to be driven by React Native Animated with useNativeDriver: true.",
"recommendation": "Use useNativeDriver: false for Lottie progress ownership unless the repo has a verified alternative."
},
{
"id": "lottie.colorfilters-contract",
"severity": "low",
"confidence": "medium",
"category": "asset",
"kind": "fileContainsBoth",
"include": "LottieView|lottie-react-native",
"also": "colorFilters|textFiltersAndroid|textFiltersIOS",
"message": "Lottie color or text filters depend on designer-authored asset keypaths.",
"recommendation": "Centralize keypaths/text targets and verify iOS/Android rendering after every designer export."
},
{
"id": "lottie.dotlottie-needs-metro",
"severity": "high",
"confidence": "high",
"category": "asset",
"kind": "dotLottieNeedsMetro",
"message": ".lottie assets are used without an obvious Metro assetExts entry.",
"recommendation": "Add 'lottie' to Metro resolver.assetExts before importing .lottie files."
},
{
"id": "lottie.dotlottie-needs-test-mock",
"severity": "low",
"confidence": "medium",
"category": "test",
"kind": "dotLottieNeedsTestMock",
"message": ".lottie assets are used in a repo with JS tests but no obvious test module mapper or mock.",
"recommendation": "Add a stable .lottie test stub for Jest/Vitest if tests import animation modules."
},
{
"id": "dotlottie.state-machine-contract",
"severity": "low",
"confidence": "medium",
"category": "asset",
"kind": "fileContainsBoth",
"include": "DotLottie|dotlottie-react-native",
"also": "stateMachineId|stateMachineStart|stateMachineLoad|stateMachineSet|stateMachineFire",
"message": "dotLottie state-machine usage depends on asset-authored IDs and inputs.",
"recommendation": "Centralize state-machine IDs/input names and validate each transition/input on iOS and Android."
},
{
"id": "dotlottie.native-runtime-needs-build",
"severity": "medium",
"confidence": "medium",
"category": "validation",
"kind": "packageHasAny",
"packages": [
"@lottiefiles/dotlottie-react-native"
],
"message": "dotLottie React Native is installed and includes native runtime code.",
"recommendation": "Use prebuild/development/EAS builds for proof; Expo Go is not sufficient for dotLottie native validation."
},
{
"id": "lottie.package-needs-validation",
"severity": "medium",
"confidence": "medium",
"category": "validation",
"kind": "packageHasAny",
"packages": [
"lottie-react-native",
"@lottiefiles/dotlottie-react-native"
],
"message": "Native Lottie dependencies are present and should be validated with focused package and platform checks.",
"recommendation": "Run the repo's package alignment, typecheck/test, and iOS/Android playback checks; include rebuild proof after native or asset changes."
}
]
};
const skipDirs = new Set([
'.git', 'node_modules', '.next', '.nuxt', 'dist', 'build', 'coverage',
'.expo', '.turbo', '.vercel', '.cache', '.codex', '.agents',
'output', 'tmp', 'temp', 'vendor', 'playwright-report', 'storybook-static',
]);
const fileExtensions = new Set([
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.css', '.scss', '.sass',
'.html', '.vue', '.svelte', '.json',
]);
const severities = ['low', 'medium', 'high'];
function usage() {
return `Usage:
scripts/audit.mjs scan [--root <path>] [--format markdown|json] [--output <path>] [--max-files <n>]
scripts/audit.mjs doctor [--root <path>] [--format markdown|json]
Options:
--root <path> Target repo root. Defaults to current working directory.
--format <format> markdown or json. Defaults to markdown.
--json Alias for --format json.
--output <path> Optional caller-chosen file path for report output.
--max-files <n> Max files to scan. Defaults to 2000.
--help Show this help.
Examples:
scripts/audit.mjs --json doctor --root .
scripts/audit.mjs scan --root . --format markdown
scripts/audit.mjs scan --root . --format json --output motion-audit.json
Config:
Optional .motion-audit.json at --root supports:
{
"ignoreRules": ["rule-id"],
"ignorePaths": ["generated/", "fixtures/"],
"ignores": [{"ruleId": "rule-id", "path": "src/example.tsx"}]
}
Inline suppression:
// motion-audit-ignore rule-id
// motion-audit-ignore all
`;
}
function requireValue(rest, flag) {
const value = rest.shift();
if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
return value;
}
function parseArgs(argv) {
const args = { command: null, root: process.cwd(), format: 'markdown', output: null, maxFiles: 2000 };
const rest = [...argv];
while (rest.length) {
const arg = rest.shift();
if (arg === '--help' || arg === '-h') args.help = true;
else if (arg === '--json') args.format = 'json';
else if (arg === '--root') args.root = path.resolve(requireValue(rest, arg));
else if (arg === '--format') args.format = requireValue(rest, arg);
else if (arg === '--output') args.output = path.resolve(requireValue(rest, arg));
else if (arg === '--max-files') args.maxFiles = Number(requireValue(rest, arg));
else if (!arg.startsWith('-') && args.command === null) args.command = arg;
else throw new Error(`Unknown argument: ${arg}`);
}
args.command = args.command ?? 'scan';
if (!['scan', 'doctor'].includes(args.command)) throw new Error(`Unknown command: ${args.command}`);
if (!['markdown', 'json'].includes(args.format)) throw new Error(`Unknown format: ${args.format}`);
if (!Number.isFinite(args.maxFiles) || args.maxFiles < 1) throw new Error('--max-files must be a positive number');
return args;
}
function loadConfig(root) {
const file = path.join(root, '.motion-audit.json');
if (!fs.existsSync(file)) return { ignoreRules: [], ignorePaths: [], ignores: [] };
try {
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
return {
ignoreRules: Array.isArray(parsed.ignoreRules) ? parsed.ignoreRules : [],
ignorePaths: Array.isArray(parsed.ignorePaths) ? parsed.ignorePaths : [],
ignores: Array.isArray(parsed.ignores) ? parsed.ignores : [],
};
} catch (error) {
throw new Error(`Failed to parse .motion-audit.json: ${error.message}`);
}
}
function shouldSkipDir(relativePath) {
return relativePath.split(path.sep).some((part) => skipDirs.has(part));
}
function readDirEntries(dir) {
try {
return fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
}
function listFiles(root, maxFiles) {
const files = [];
function walk(dir) {
if (files.length >= maxFiles) return;
for (const entry of readDirEntries(dir)) {
const full = path.join(dir, entry.name);
const rel = path.relative(root, full);
if (entry.isDirectory()) {
if (!shouldSkipDir(rel)) walk(full);
} else if (entry.isFile() && fileExtensions.has(path.extname(entry.name))) {
files.push(full);
}
if (files.length >= maxFiles) return;
}
}
walk(root);
return files;
}
function listAllFiles(root, maxFiles) {
const files = [];
function walk(dir) {
if (files.length >= maxFiles) return;
for (const entry of readDirEntries(dir)) {
const full = path.join(dir, entry.name);
const rel = path.relative(root, full);
if (entry.isDirectory()) {
if (!shouldSkipDir(rel)) walk(full);
} else if (entry.isFile()) {
files.push(full);
}
if (files.length >= maxFiles) return;
}
}
walk(root);
return files;
}
function listPackageFiles(root) {
const files = [];
function walk(dir) {
for (const entry of readDirEntries(dir)) {
const full = path.join(dir, entry.name);
const rel = path.relative(root, full);
if (entry.isDirectory()) {
if (!shouldSkipDir(rel)) walk(full);
} else if (entry.isFile() && entry.name === 'package.json') {
files.push(full);
}
}
}
walk(root);
return files;
}
function readPackage(root) {
if (!fs.existsSync(root)) return { exists: false, packages: new Set(), packageVersions: {}, packageFiles: [], scripts: {} };
const files = listPackageFiles(root);
const packages = new Set();
const packageVersions = {};
const packageFiles = [];
const scripts = {};
for (const file of files) {
try {
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
Object.assign(scripts, pkg.scripts ?? {});
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies, pkg.peerDependencies, pkg.optionalDependencies);
const packageNames = new Set(Object.keys(deps ?? {}));
Object.assign(packageVersions, deps ?? {});
for (const name of packageNames) packages.add(name);
packageFiles.push({ file: path.relative(root, file), packages: packageNames, packageVersions: deps ?? {} });
} catch {
continue;
}
}
return { exists: files.length > 0, packages, packageVersions, packageFiles, scripts };
}
function lineForIndex(text, index) {
return text.slice(0, index).split('\n').length;
}
function excerptForLine(lines, lineNumber) {
return (lines[lineNumber - 1] ?? '').trim().slice(0, 240);
}
function isIgnored(config, ruleId, relativePath, lines, lineNumber) {
if (config.ignoreRules.includes(ruleId)) return true;
if (config.ignorePaths.some((ignored) => relativePath.includes(ignored))) return true;
if (config.ignores.some((entry) => entry?.ruleId === ruleId && typeof entry.path === 'string' && entry.path.length > 0 && relativePath.includes(entry.path))) return true;
const nearby = [lines[lineNumber - 1], lines[lineNumber - 2]].filter(Boolean).join('\n');
return nearby.includes('motion-audit-ignore all') || nearby.includes(`motion-audit-ignore ${ruleId}`);
}
function makeFinding(rule, relativePath, line, excerpt) {
return {
id: `${rule.id}:${relativePath}:${line}`,
ruleId: rule.id,
severity: rule.severity,
confidence: rule.confidence,
category: rule.category,
file: relativePath,
line,
excerpt,
rationale: rule.message,
recommendation: rule.recommendation,
};
}
function ruleRegex(pattern) {
return new RegExp(pattern, 'gims');
}
function dotLottieEvidence(root, textFiles) {
const evidence = [];
for (const file of textFiles) {
let text;
try {
text = fs.readFileSync(file, 'utf8');
} catch {
continue;
}
for (const match of text.matchAll(/(?:require\(\s*['"`][^'"`\n]+\.lottie['"`]\s*\)|import\s+(?:[^'"`;]+?\s+from\s+)?['"`][^'"`\n]+\.lottie['"`])/g)) {
const line = lineForIndex(text, match.index ?? 0);
evidence.push({
file: path.relative(root, file),
line,
excerpt: excerptForLine(text.split('\n'), line),
});
}
}
return evidence;
}
function hasMetroDotLottieSupport(allFiles) {
const names = [
'metro.config.js',
'metro.config.cjs',
'metro.config.mjs',
'metro.config.ts',
];
return allFiles.some((file) => {
if (!names.includes(path.basename(file))) return false;
const text = fs.readFileSync(file, 'utf8');
return assetExtsContains(text, 'lottie');
});
}
function assetExtsContains(text, extension) {
const escaped = extension.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`assetExts\\.(?:push|unshift)\\(\\s*['"\`]${escaped}['"\`]`, 'i').test(text)
|| new RegExp(`assetExts\\s*[:=]\\s*\\[[^\\]]*['"\`]${escaped}['"\`][^\\]]*\\]`, 'i').test(text);
}
function hasJsTestSurface(pkg, allFiles) {
const scriptText = Object.entries(pkg.scripts ?? {})
.map(([name, value]) => `${name}:${value}`)
.join('\n');
if (/\b(jest|vitest)\b/.test(scriptText)) return true;
return allFiles.some((file) => {
const name = path.basename(file);
return (
/^jest\.config\.[cm]?[jt]s$/.test(name) ||
/^vitest\.config\.[cm]?[jt]s$/.test(name)
);
});
}
function hasDotLottieTestMock(root, allFiles) {
const configNames = new Set([
'jest.config.js',
'jest.config.cjs',
'jest.config.mjs',
'jest.config.ts',
'vitest.config.js',
'vitest.config.cjs',
'vitest.config.mjs',
'vitest.config.ts',
]);
for (const file of allFiles) {
const name = path.basename(file);
if (!configNames.has(name) && !file.includes(`${path.sep}__mocks__${path.sep}`)) {
continue;
}
let text;
try {
text = fs.readFileSync(file, 'utf8');
} catch {
continue;
}
if (/\\\.\(lottie\)|\\\.lottie|\.lottie|dotLottieMock|lottieMock|lottie-test-file-stub/i.test(text)) {
return true;
}
}
return false;
}
function scanRule(rule, file, root, text, config) {
const relativePath = path.relative(root, file);
const lines = text.split('\n');
const findings = [];
if (rule.kind === 'fileContainsWithout' || rule.kind === 'fileContainsBoth' || rule.kind === 'fileContainsBothWithout') {
const includeMatch = ruleRegex(rule.include).exec(text);
const alsoMatch = rule.also ? ruleRegex(rule.also).exec(text) : null;
const withoutMatch = rule.without ? ruleRegex(rule.without).exec(text) : null;
const matches =
rule.kind === 'fileContainsBoth'
? includeMatch && alsoMatch
: rule.kind === 'fileContainsBothWithout'
? includeMatch && alsoMatch && !withoutMatch
: includeMatch && (!rule.also || alsoMatch) && !withoutMatch;
if (matches) {
const index = includeMatch.index;
const line = lineForIndex(text, index);
if (!isIgnored(config, rule.id, relativePath, lines, line)) {
findings.push(makeFinding(rule, relativePath, line, excerptForLine(lines, line)));
}
}
return findings;
}
if (rule.kind === 'packageHasAny') return findings;
const regex = ruleRegex(rule.pattern);
for (const match of text.matchAll(regex)) {
const line = lineForIndex(text, match.index ?? 0);
if (!isIgnored(config, rule.id, relativePath, lines, line)) {
findings.push(makeFinding(rule, relativePath, line, excerptForLine(lines, line)));
}
}
return findings;
}
function scan(root, maxFiles) {
if (!fs.existsSync(root)) throw new Error(`Root does not exist: ${root}`);
const config = loadConfig(root);
const files = listFiles(root, maxFiles);
const allFiles = listAllFiles(root, Number.POSITIVE_INFINITY);
const pkg = readPackage(root);
const findings = [];
for (const rule of profile.rules) {
if (config.ignoreRules.includes(rule.id)) continue;
if (rule.kind === 'packageHasAny') {
for (const packageFile of pkg.packageFiles) {
const matched = (rule.packages ?? []).filter((name) => packageFile.packages.has(name));
if (matched.length === 0) continue;
if (isIgnored(config, rule.id, packageFile.file, [''], 1)) continue;
const matchedWithVersions = matched.map((name) => {
const version = packageFile.packageVersions?.[name];
return version ? `${name}@${version}` : name;
});
findings.push({
id: `${rule.id}:${packageFile.file}:1`,
ruleId: rule.id,
severity: rule.severity,
confidence: rule.confidence,
category: rule.category,
file: packageFile.file,
line: 1,
excerpt: `matched packages: ${matchedWithVersions.join(', ')}`,
rationale: rule.message,
recommendation: rule.recommendation,
});
}
continue;
}
if (rule.kind === 'dotLottieNeedsMetro') {
const evidence = dotLottieEvidence(root, files)
.find((entry) => !isIgnored(config, rule.id, entry.file, [''], entry.line));
if (evidence && !hasMetroDotLottieSupport(allFiles)) {
findings.push(makeFinding(rule, evidence.file, evidence.line, evidence.excerpt));
}
continue;
}
if (rule.kind === 'dotLottieNeedsTestMock') {
const evidence = dotLottieEvidence(root, files)
.find((entry) => !isIgnored(config, rule.id, entry.file, [''], entry.line));
if (
evidence &&
hasJsTestSurface(pkg, allFiles) &&
!hasDotLottieTestMock(root, allFiles)
) {
findings.push(makeFinding(rule, evidence.file, evidence.line, evidence.excerpt));
}
continue;
}
for (const file of files) {
let text;
try {
text = fs.readFileSync(file, 'utf8');
} catch {
continue;
}
findings.push(...scanRule(rule, file, root, text, config));
}
}
return {
ok: !findings.some((finding) => finding.severity === 'high'),
profile: profile.skillName,
root,
scannedFiles: files.length,
rules: profile.rules.length,
findings,
summary: severities.reduce((acc, severity) => {
acc[severity] = findings.filter((finding) => finding.severity === severity).length;
return acc;
}, {}),
};
}
function doctor(root, maxFiles) {
const pkg = readPackage(root);
const lottiePackages = [
'lottie-react-native',
'@lottiefiles/dotlottie-react-native',
]
.filter((name) => pkg.packages.has(name))
.map((name) => `${name}@${pkg.packageVersions?.[name] ?? 'unknown'}`);
return {
ok: fs.existsSync(root),
profile: profile.skillName,
root,
packageJson: pkg.exists,
lottiePackages,
configuredRules: profile.rules.length,
sampleFileCount: fs.existsSync(root) ? listFiles(root, maxFiles).length : 0,
configFile: fs.existsSync(path.join(root, '.motion-audit.json')),
notes: [
'scan is read-only',
'Lottie findings are triage; verify against real code and asset contracts',
'use --format json for machine-readable output',
'findings can be suppressed with .motion-audit.json or inline motion-audit-ignore comments',
],
};
}
function renderMarkdown(result) {
if (result.sampleFileCount !== undefined) {
return `# Motion Audit Doctor: ${result.profile}
- Root: ${result.root}
- Package JSON: ${result.packageJson ? 'yes' : 'no'}
- Lottie packages: ${result.lottiePackages.length ? result.lottiePackages.join(', ') : 'none detected'}
- Config file: ${result.configFile ? 'yes' : 'no'}
- Configured rules: ${result.configuredRules}
- Sample file count: ${result.sampleFileCount}
- Status: ${result.ok ? 'ok' : 'failed'}
`;
}
const findings = result.findings
.map((finding) => `## ${finding.severity.toUpperCase()} ${finding.ruleId}
- File: ${finding.file}:${finding.line}
- Confidence: ${finding.confidence}
- Category: ${finding.category}
- Evidence: ${finding.excerpt || '(file-level match)'}
- Rationale: ${finding.rationale}
- Recommendation: ${finding.recommendation}
`)
.join('\n');
return `# Motion Audit Report: ${result.profile}
- Root: ${result.root}
- Scanned files: ${result.scannedFiles}
- Rules: ${result.rules}
- Findings: ${result.findings.length}
- Severity summary: high=${result.summary.high}, medium=${result.summary.medium}, low=${result.summary.low}
- Status: ${result.ok ? 'no high-severity findings' : 'high-severity findings present'}
${findings || 'No findings.'}
`;
}
function emit(result, args) {
const body = args.format === 'json' ? JSON.stringify(result, null, 2) + '\n' : renderMarkdown(result);
if (args.output) {
fs.mkdirSync(path.dirname(args.output), { recursive: true });
fs.writeFileSync(args.output, body);
} else {
process.stdout.write(body);
}
}
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
process.stdout.write(usage());
process.exit(0);
}
const result = args.command === 'doctor' ? doctor(args.root, args.maxFiles) : scan(args.root, args.maxFiles);
emit(result, args);
process.exit(result.ok ? 0 : 2);
} catch (error) {
const payload = { ok: false, profile: profile.skillName, error: error.message };
const wantsJson = process.argv.includes('--json') || process.argv.includes('--format') && process.argv.includes('json');
if (wantsJson) process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
else {
console.error(error.message);
console.error(usage());
}
process.exit(1);
}