Skip to content

Comments

Lottie Animation Integration for MotionForge#9

Merged
codedbytahir merged 1 commit intomainfrom
feature/lottie-integration-209608830295865078
Feb 15, 2026
Merged

Lottie Animation Integration for MotionForge#9
codedbytahir merged 1 commit intomainfrom
feature/lottie-integration-209608830295865078

Conversation

@codedbytahir
Copy link
Owner

@codedbytahir codedbytahir commented Feb 15, 2026

Implemented a production-grade Lottie animation system for MotionForge. The system ensures frame-perfect rendering by synchronizing lottie-web with the internal MotionForge frame clock, disabling standard time-based autoplay. It supports advanced features like playback segments (slicing), variable playback rates, and automatic sequence mapping. The CLI and documentation were also updated to provide a seamless developer experience.


PR created automatically by Jules for task 209608830295865078 started by @codedbytahir

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Lottie animation component with deterministic frame synchronization.
    • Supports remote URLs and inlined JSON animation data.
    • Configurable playback rate, frame start/end ranges, looping, and dimensions.
  • Documentation

    • Added comprehensive Lottie integration guide with usage examples and performance optimization tips.
  • Version

    • Updated to version 1.3.0.

- Created <Lottie /> component with deterministic frame synchronization.
- Added support for remote JSON URLs and local JSON objects.
- Implemented frameStart, frameEnd, playbackRate, and loop props.
- Integrated Lottie with Sequence relative frame system.
- Updated create-motionforge templates with Lottie support.
- Added DemoLottie composition for examples and testing.
- Bumped version to 1.3.0 and updated CHANGELOG.md.
- Updated README with comprehensive Lottie documentation.
- Optimized performance with memoization and instance cleanup.
- Ensured SSR compatibility.
- Fixed AbsoluteFill type definition to allow optional children.

Co-authored-by: codedbytahir <200578194+codedbytahir@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@netlify
Copy link

netlify bot commented Feb 15, 2026

Deploy Preview for motion-forge ready!

Name Link
🔨 Latest commit 8d34a47
🔍 Latest deploy log https://app.netlify.com/projects/motion-forge/deploys/69919052df6abc0008ac4977
😎 Deploy Preview https://deploy-preview-9--motion-forge.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

This PR integrates Lottie animations into MotionForge by adding a new Lottie component with frame-synced playback, lottie-web dependency, demo components, template updates, type refinements, and documentation.

Changes

Cohort / File(s) Summary
Dependency Additions
package.json, packages/motionforge/package.json, packages/create-motionforge/templates/shared/package.json.template
Added lottie-web ^5.13.0 as runtime dependency; version bump to 1.3.0 in packages/motionforge/package.json.
Lottie Component Implementation
packages/motionforge/src/components/Lottie.tsx, src/lib/remotion/components/Lottie.tsx
New Lottie component with frame synchronization via MotionForge context, lottie-web initialization, playback control (playbackRate, loop, frameStart/frameEnd), error handling, and deterministic animation mapping.
Demo Components
packages/motionforge/src/demo/DemoLottie.tsx, src/lib/remotion/demo/DemoLottie.tsx
New demo components showcasing Lottie integration with multiple animation sequences, dynamic background effects, and remote/local JSON sources.
Public API Exports
packages/motionforge/src/index.ts, src/lib/remotion/index.ts
Exported Lottie component and LottieProps type from new barrel exports.
Type Refinements
packages/motionforge/src/components/Media.tsx, src/lib/remotion/components/Media.tsx
Made AbsoluteFillProps children property optional (React.ReactNode → React.ReactNode\?).
Context Updates
packages/motionforge/src/components/Sequence.tsx, src/lib/remotion/components/Sequence.tsx
Changed RelativeFrameContext type from number to number | null; hook now returns nullable value.
Performance Hook Type Refinement
src/lib/remotion/hooks/performance.ts
Tightened extrapolateLeft/extrapolateRight types to specific string literals ('clamp' | 'extend' | 'identity').
CLI Templates
packages/create-motionforge/templates/hello-world/page.tsx.template
Extended imports to include Lottie and Sequence; added zIndex:10 to main container; introduced Lottie animation sequence with remote JSON source, playbackRate 0.5, and loop support.
App Integration
src/app/page.tsx
Added DemoLottie import and new composition entry for Lottie demo with 300-frame duration.
Documentation
packages/motionforge/README.md, packages/motionforge/CHANGELOG.md
Added comprehensive Lottie integration guide (Basic Usage, Advanced Configuration, Sequence Syncing, Performance Tips); introduced 1.3.0 changelog entry documenting new Lottie features.

Sequence Diagram(s)

sequenceDiagram
    participant React as React Component
    participant MotionForge as MotionForge Context
    participant Lottie as Lottie Component
    participant LottieWeb as lottie-web Library

    React->>Lottie: Mount with props (src, frameStart, frameEnd)
    Lottie->>LottieWeb: Initialize animation (SVG renderer, no autoplay)
    LottieWeb-->>Lottie: Animation ready (DOMLoaded/data_ready)
    Lottie->>Lottie: Set isLoaded = true

    loop On Each Frame
        MotionForge->>Lottie: Provide currentFrame (absolute or relative)
        Lottie->>Lottie: Compute targetFrame (playbackRate, frameStart/frameEnd, loop)
        Lottie->>LottieWeb: goToAndStop(mappedFrame, true)
        LottieWeb-->>React: Render frame
    end

    React->>Lottie: Unmount
    Lottie->>LottieWeb: Destroy animation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hop, frame by frame, animations now dance,
Lottie joins MotionForge in a synchronized prance,
From remote JSON streams to looped delight,
Frame-synced playback gets the magic just right! ✨🎬

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Lottie Animation Integration for MotionForge' clearly and concisely summarizes the main change: adding Lottie animation support to MotionForge, which aligns with all the file changes and PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/lottie-integration-209608830295865078

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In `@packages/create-motionforge/templates/hello-world/page.tsx.template`:
- Around line 49-53: The page.tsx.template currently uses an external Lottie URL
in the <Lottie> component which is fragile; replace that external src usage by
bundling an inline Lottie JSON (like the simpleLottie object defined in
DemoLottie.tsx) and pass it to the Lottie component (or import
DemoLottie/simpleLottie) so the starter template renders reliably offline and
matches the demo; update the <Lottie> usage in page.tsx.template to accept the
inlined JSON instead of the hardcoded "https://assets.lottiefiles.com/..." URL
and keep the existing playbackRate and loop props.

In `@packages/create-motionforge/templates/shared/package.json.template`:
- Around line 12-13: Update the package version for the "motionforge" dependency
in package.json.template from "^1.2.0" to "^1.3.0" so it is compatible with the
included "lottie-web" dependency; locate the "motionforge" entry in the shared
package.json.template and bump the version string to "^1.3.0" (ensure the
trailing comma and JSON formatting remain valid).

In `@packages/motionforge/CHANGELOG.md`:
- Line 8: Update the release date in the changelog header for version marker "##
[1.3.0] - 2024-03-20" to the correct release date (replace 2024-03-20 with
2026-02-15) so the header reads "## [1.3.0] - 2026-02-15"; locate the exact
header line containing "## [1.3.0]" in CHANGELOG.md and change only the date
portion.

In `@packages/motionforge/src/components/Lottie.tsx`:
- Around line 143-157: The mapping of currentFrame to Lottie frames can produce
negative targetFrame when playbackRate is negative and the modulo preserves
sign; update the targetFrame calculation in the Lottie component (symbols:
currentFrame, playbackRate, loop, duration, start, targetFrame, finalFrame,
anim.goToAndStop) so that after computing targetFrame = currentFrame *
playbackRate you: for loop=true normalize the modulo to a non-negative value
(e.g. ((targetFrame % duration) + duration) % duration) and for loop=false clamp
to the [0, duration - 0.01] range (use Math.max(0, ...) or equivalent), then
compute finalFrame = start + targetFrame and call anim.goToAndStop(finalFrame,
true).
- Around line 80-125: The effect currently re-initializes whenever the inline
object `src` reference changes, causing infinite re-creates and never clearing
prior errors; update the effect in the component that uses useEffect (the effect
that references `src`, `containerRef`, `animationRef`, `setIsLoaded`, and
`setError`) to compute a stable key for `src` (e.g., use string key = typeof src
=== 'string' ? src : JSON.stringify(src)), track the previous key with a ref and
bail out early if the key hasn't changed, and when the key does change reset
state by calling setError(null) and setIsLoaded(false) before creating the new
lottie instance; then use that stable key (not the raw object) in the effect
dependency array so inline object props no longer force re-initialization.

In `@src/lib/remotion/components/Lottie.tsx`:
- Around line 143-154: The targetFrame calculation can become negative when
relativeFrame/currentFrame is negative; update the mapping in the Lottie
component so targetFrame is clamped to a non-negative value before applying
looping or duration bounds. Specifically, in the block computing targetFrame
(symbols: currentFrame, playbackRate, targetFrame, loop, duration) ensure you
compute targetFrame = Math.max(0, currentFrame * playbackRate) (or equivalent)
and then apply the loop modulo or Math.min clipping, and ensure finalFrame
(symbols: start, finalFrame) cannot end up before start by preserving that
clamp. This keeps defensive safety for edge cases where relativeFrame may be
negative.
- Around line 80-125: When src changes, reset the loading state at the start of
the init effect to avoid stale true values: inside the useEffect that depends on
src (the effect creating the lottie AnimationItem and using animationRef), call
setIsLoaded(false) before initiating load, and also clear any previous setError
if present; ensure the onLoaded callback still sets setIsLoaded(true) when
DOMLoaded/data_ready fire and that cleanup still destroys anim and nulls
animationRef.current to prevent races.
- Line 125: The effect in the Lottie component currently lists src in its
dependency array which causes infinite re-initialization when callers pass an
inline object; update the hook to stabilize the dependency by deriving a
serialized key (e.g. const srcKey = useMemo(() => typeof src === 'string' ? src
: JSON.stringify(src), [src]) or compute a ref-based deep-compare) and use
srcKey in the effect dependency instead of src (or alternatively document that
callers must memoize src); locate the effect in the Lottie component that
currently ends with "}, [src]);" and replace the dependency with the derived
srcKey so the animation is not destroyed/recreated on every render.

In `@src/lib/remotion/components/Sequence.tsx`:
- Around line 110-112: The exported type for useRelativeCurrentFrame is
incorrect in the distributed types; update the declaration so
useRelativeCurrentFrame returns number | null to match the implementation that
uses RelativeFrameContext (createContext<number | null>) and the hook
useRelativeCurrentFrame; find the exported type/signature for
useRelativeCurrentFrame in the generated declaration file (the dist/index.d.ts
export for useRelativeCurrentFrame) and change its return type from number to
number | null so external consumers see the nullable return.
🧹 Nitpick comments (6)
packages/motionforge/package.json (1)

76-78: Consider making lottie-web a peer or optional dependency.

lottie-web is a sizable library (~250KB+ minified). As a hard dependency, it's always installed even for users who never use the <Lottie /> component. Making it a peerDependency (or optionalDependency) would let consumers opt in, keeping the default install leaner.

♻️ Suggested change
-  "dependencies": {
-    "lottie-web": "^5.13.0"
-  },
+  "peerDependencies": {
+    "react": ">=18.0.0",
+    "react-dom": ">=18.0.0",
+    "lottie-web": ">=5.12.0"
+  },
+  "peerDependenciesMeta": {
+    "lottie-web": {
+      "optional": true
+    }
+  },
packages/motionforge/src/components/Lottie.tsx (2)

121-124: Synchronous setError in useEffect catch block (ESLint react-hooks/set-state-in-effect).

The ESLint hint is valid — setError is called synchronously in the effect body (not in a callback/event). While this particular case (inside a catch) is unlikely to cause cascading renders in practice, you can address the lint violation by deferring:

Proposed fix
     } catch (err) {
-      setError(err instanceof Error ? err.message : String(err));
-      return () => {};
+      const msg = err instanceof Error ? err.message : String(err);
+      queueMicrotask(() => {
+        if (!isCancelled) setError(msg);
+      });
+      return () => { isCancelled = true; };
     }

85-94: Avoid any for the Lottie params object.

params: any drops all type safety. Consider using the AnimationConfigWithPath / AnimationConfigWithData union from lottie-web types, or at least a more specific inline type.

packages/motionforge/src/demo/DemoLottie.tsx (1)

1-130: Near-identical duplication with src/lib/remotion/demo/DemoLottie.tsx.

These two files are effectively copy-pasted (only the import path differs). Consider extracting the shared demo into a single location and re-exporting, or generating one from the other, to avoid the two files drifting apart over time.

src/lib/remotion/components/Lottie.tsx (2)

121-123: ESLint: synchronous setState in effect catch block.

The static analysis flags setError called synchronously inside the effect body. In this case it's in a catch after a synchronous loadAnimation call, so it won't loop, but you could move the error into a ref and trigger a re-render via a separate mechanism, or simply suppress the rule here with an inline disable comment if you consider it acceptable.


160-164: style object in useMemo deps may defeat memoization.

Similar to the src issue, if the caller passes an inline style object, the reference changes every render, causing containerStyle to recompute. This is less critical than the src issue (no teardown/rebuild), but worth noting for consistency.

Comment on lines +49 to +53
<Lottie
src="https://assets.lottiefiles.com/packages/lf20_u4j3X6.json"
playbackRate={0.5}
loop
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

External Lottie URL in a starter template is fragile and inconsistent with the demo.

This template hardcodes https://assets.lottiefiles.com/... while the DemoLottie files use https://assets10.lottiefiles.com/... (different subdomain). If either URL goes stale, users see a broken hello-world experience on first run.

Consider bundling a small inline JSON animation (like the simpleLottie object used in DemoLottie.tsx) instead of relying on an external URL for the starter template.

🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/hello-world/page.tsx.template` around
lines 49 - 53, The page.tsx.template currently uses an external Lottie URL in
the <Lottie> component which is fragile; replace that external src usage by
bundling an inline Lottie JSON (like the simpleLottie object defined in
DemoLottie.tsx) and pass it to the Lottie component (or import
DemoLottie/simpleLottie) so the starter template renders reliably offline and
matches the demo; update the <Lottie> usage in page.tsx.template to accept the
inlined JSON instead of the hardcoded "https://assets.lottiefiles.com/..." URL
and keep the existing playbackRate and loop props.

Comment on lines 12 to +13
"motionforge": "^1.2.0",
"lottie-web": "^5.13.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Template references motionforge ^1.2.0 but Lottie requires ^1.3.0.

The template now includes lottie-web and presumably uses the new <Lottie /> component, but the motionforge dependency is still pinned to ^1.2.0. Projects scaffolded from this template could resolve to 1.2.x (which lacks Lottie support) if 1.3.0 isn't yet published.

♻️ Suggested fix
-    "motionforge": "^1.2.0",
+    "motionforge": "^1.3.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"motionforge": "^1.2.0",
"lottie-web": "^5.13.0",
"motionforge": "^1.3.0",
"lottie-web": "^5.13.0",
🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/shared/package.json.template` around
lines 12 - 13, Update the package version for the "motionforge" dependency in
package.json.template from "^1.2.0" to "^1.3.0" so it is compatible with the
included "lottie-web" dependency; locate the "motionforge" entry in the shared
package.json.template and bump the version string to "^1.3.0" (ensure the
trailing comma and JSON formatting remain valid).

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2024-03-20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect changelog date.

The date 2024-03-20 appears to be incorrect — this PR was created on 2026-02-15. Update to the actual release date.

-## [1.3.0] - 2024-03-20
+## [1.3.0] - 2026-02-15
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## [1.3.0] - 2024-03-20
## [1.3.0] - 2026-02-15
🤖 Prompt for AI Agents
In `@packages/motionforge/CHANGELOG.md` at line 8, Update the release date in the
changelog header for version marker "## [1.3.0] - 2024-03-20" to the correct
release date (replace 2024-03-20 with 2026-02-15) so the header reads "##
[1.3.0] - 2026-02-15"; locate the exact header line containing "## [1.3.0]" in
CHANGELOG.md and change only the date portion.

Comment on lines +80 to +125
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current) return;

let isCancelled = false;

const params: any = {
container: containerRef.current,
renderer: 'svg',
loop: false,
autoplay: false,
rendererSettings: {
progressiveLoad: false,
hideOnTransparent: true,
}
};

if (typeof src === 'string') {
params.path = src;
} else {
params.animationData = src;
}

try {
const anim = lottie.loadAnimation(params);
animationRef.current = anim;

const onLoaded = () => {
if (!isCancelled) {
setIsLoaded(true);
}
};

// Both events can be useful depending on how Lottie is loaded
anim.addEventListener('DOMLoaded', onLoaded);
anim.addEventListener('data_ready', onLoaded);

return () => {
isCancelled = true;
anim.destroy();
animationRef.current = null;
};
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return () => {};
}
}, [src]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Object src in dependency array causes re-initialization on every render.

When src is an inline object (e.g., <Lottie src={{ ... }} />), a new reference is created each render, causing useEffect to destroy and re-create the lottie instance in an infinite loop. This is the primary usage pattern shown in the demos (src={simpleLottie} is fine since it's module-scoped, but any consumer passing an inline object will hit this).

Additionally, the error state is never cleared when src changes, so after a failed load, switching to a valid src still shows the error UI.

Proposed fix
+  const srcRef = useRef(src);
+  const [srcKey, setSrcKey] = useState(0);
+
+  // Only re-trigger when src *actually* changes (deep-compare for objects)
+  useEffect(() => {
+    if (typeof src === 'string') {
+      if (src !== srcRef.current) {
+        srcRef.current = src;
+        setSrcKey(k => k + 1);
+      }
+    } else {
+      // For objects, JSON-serialize or use a stable reference from the caller
+      srcRef.current = src;
+    }
+  }, [src]);

   // Handle initialization
   useEffect(() => {
     if (typeof window === 'undefined' || !containerRef.current) return;

     let isCancelled = false;
+    setError(null);
+    setIsLoaded(false);
     // ... rest unchanged
-  }, [src]);
+  }, [srcKey]);

Alternatively, document that src must be a stable reference (module-level constant or useMemo'd) and add a note in the JSDoc. A simpler approach is to JSON.stringify the object src for the dep array:

+  const srcStable = useMemo(
+    () => (typeof src === 'string' ? src : JSON.stringify(src)),
+    [src]
+  );
   // ...
-  }, [src]);
+  }, [srcStable]);

But note JSON.stringify itself re-runs on every render for object src, so the caller should ideally stabilize the reference. At minimum, clear error/loaded state on re-init.

🧰 Tools
🪛 ESLint

[error] 122-122: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/packages/motionforge/src/components/Lottie.tsx:122:7
120 | };
121 | } catch (err) {

122 | setError(err instanceof Error ? err.message : String(err));
| ^^^^^^^^ Avoid calling setState() directly within an effect
123 | return () => {};
124 | }
125 | }, [src]);

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 80 - 125, The
effect currently re-initializes whenever the inline object `src` reference
changes, causing infinite re-creates and never clearing prior errors; update the
effect in the component that uses useEffect (the effect that references `src`,
`containerRef`, `animationRef`, `setIsLoaded`, and `setError`) to compute a
stable key for `src` (e.g., use string key = typeof src === 'string' ? src :
JSON.stringify(src)), track the previous key with a ref and bail out early if
the key hasn't changed, and when the key does change reset state by calling
setError(null) and setIsLoaded(false) before creating the new lottie instance;
then use that stable key (not the raw object) in the effect dependency array so
inline object props no longer force re-initialization.

Comment on lines +143 to +157
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;

if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}

// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;

// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Frame mapping doesn't clamp negative values, breaking with negative playbackRate or edge cases.

  • Line 145: currentFrame * playbackRate can be negative if playbackRate < 0 (not validated).
  • Line 148: JS % operator preserves the sign of the dividend, so a negative targetFrame % duration yields a negative result, causing finalFrame to fall below start.
  • Line 150: Non-loop path uses Math.min but no Math.max(0, ...), so negative values pass through unclamped.
Proposed fix
     let targetFrame = currentFrame * playbackRate;

     if (loop) {
-      targetFrame = targetFrame % duration;
+      targetFrame = ((targetFrame % duration) + duration) % duration;
     } else {
-      targetFrame = Math.min(targetFrame, duration - 0.01);
+      targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;
if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;
if (loop) {
targetFrame = ((targetFrame % duration) + duration) % duration;
} else {
targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01));
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
// goToAndStop(value, isFrame) - second arg true means it's a frame number, not time
anim.goToAndStop(finalFrame, true);
🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 143 - 157, The
mapping of currentFrame to Lottie frames can produce negative targetFrame when
playbackRate is negative and the modulo preserves sign; update the targetFrame
calculation in the Lottie component (symbols: currentFrame, playbackRate, loop,
duration, start, targetFrame, finalFrame, anim.goToAndStop) so that after
computing targetFrame = currentFrame * playbackRate you: for loop=true normalize
the modulo to a non-negative value (e.g. ((targetFrame % duration) + duration) %
duration) and for loop=false clamp to the [0, duration - 0.01] range (use
Math.max(0, ...) or equivalent), then compute finalFrame = start + targetFrame
and call anim.goToAndStop(finalFrame, true).

Comment on lines +80 to +125
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current) return;

let isCancelled = false;

const params: any = {
container: containerRef.current,
renderer: 'svg',
loop: false,
autoplay: false,
rendererSettings: {
progressiveLoad: false,
hideOnTransparent: true,
}
};

if (typeof src === 'string') {
params.path = src;
} else {
params.animationData = src;
}

try {
const anim = lottie.loadAnimation(params);
animationRef.current = anim;

const onLoaded = () => {
if (!isCancelled) {
setIsLoaded(true);
}
};

// Both events can be useful depending on how Lottie is loaded
anim.addEventListener('DOMLoaded', onLoaded);
anim.addEventListener('data_ready', onLoaded);

return () => {
isCancelled = true;
anim.destroy();
animationRef.current = null;
};
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return () => {};
}
}, [src]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

isLoaded is not reset when src changes, causing stale reads on the new animation.

When src changes, the cleanup (line 117-119) destroys the old animation and nulls the ref, but isLoaded remains true. The new effect body immediately assigns the fresh (not-yet-loaded) AnimationItem to animationRef.current (line 104). Because isLoaded is still true, the frame-sync effect (line 128) will fire and call anim.totalFrames / anim.goToAndStop on an animation whose data hasn't loaded yet, which can produce incorrect rendering or throw.

Proposed fix — reset loading state at the top of the init effect
   useEffect(() => {
     if (typeof window === 'undefined' || !containerRef.current) return;
 
+    setIsLoaded(false);
+    setError(null);
+
     let isCancelled = false;
🧰 Tools
🪛 ESLint

[error] 122-122: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/src/lib/remotion/components/Lottie.tsx:122:7
120 | };
121 | } catch (err) {

122 | setError(err instanceof Error ? err.message : String(err));
| ^^^^^^^^ Avoid calling setState() directly within an effect
123 | return () => {};
124 | }
125 | }, [src]);

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` around lines 80 - 125, When src
changes, reset the loading state at the start of the init effect to avoid stale
true values: inside the useEffect that depends on src (the effect creating the
lottie AnimationItem and using animationRef), call setIsLoaded(false) before
initiating load, and also clear any previous setError if present; ensure the
onLoaded callback still sets setIsLoaded(true) when DOMLoaded/data_ready fire
and that cleanup still destroys anim and nulls animationRef.current to prevent
races.

setError(err instanceof Error ? err.message : String(err));
return () => {};
}
}, [src]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Object src as a dependency causes infinite re-initialization.

When src is an inline object literal (e.g. <Lottie src={{...}} />), a new reference is created every render, so this effect re-runs on every frame — destroying and recreating the animation each time. Consider either documenting that callers must memoize the object, or stabilizing the dependency internally (e.g. via JSON.stringify).

Option: stabilize with a serialized key
+ const srcKey = useMemo(
+   () => (typeof src === 'string' ? src : JSON.stringify(src)),
+   [src],
+ );
+
  useEffect(() => {
    // ...init...
- }, [src]);
+ }, [srcKey]);

Note: useMemo with [src] alone still compares by reference, so JSON.stringify would need to be moved outside or use a useRef-based deep-compare. A simpler approach is to document/enforce that callers pass a stable reference.

🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` at line 125, The effect in the Lottie
component currently lists src in its dependency array which causes infinite
re-initialization when callers pass an inline object; update the hook to
stabilize the dependency by deriving a serialized key (e.g. const srcKey =
useMemo(() => typeof src === 'string' ? src : JSON.stringify(src), [src]) or
compute a ref-based deep-compare) and use srcKey in the effect dependency
instead of src (or alternatively document that callers must memoize src); locate
the effect in the Lottie component that currently ends with "}, [src]);" and
replace the dependency with the derived srcKey so the animation is not
destroyed/recreated on every render.

Comment on lines +143 to +154
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;

if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}

// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Frame mapping can produce negative Lottie frames when currentFrame is 0 and playbackRate is applied.

The mapping targetFrame = currentFrame * playbackRate is correct for non-negative currentFrame, but if relativeFrame happens to be negative (e.g., due to timing edge cases in Sequence), targetFrame will be negative and finalFrame will be before start. Consider clamping:

-    let targetFrame = currentFrame * playbackRate;
+    let targetFrame = Math.max(0, currentFrame * playbackRate);

This is a minor edge case since Sequence doesn't render children when inactive, but it adds defensive safety.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = currentFrame * playbackRate;
if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
// Map MotionForge currentFrame to Lottie frame
// playbackRate affects how fast we move through the Lottie timeline
let targetFrame = Math.max(0, currentFrame * playbackRate);
if (loop) {
targetFrame = targetFrame % duration;
} else {
targetFrame = Math.min(targetFrame, duration - 0.01);
}
// finalFrame is relative to the Lottie's original timeline
const finalFrame = start + targetFrame;
🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` around lines 143 - 154, The
targetFrame calculation can become negative when relativeFrame/currentFrame is
negative; update the mapping in the Lottie component so targetFrame is clamped
to a non-negative value before applying looping or duration bounds.
Specifically, in the block computing targetFrame (symbols: currentFrame,
playbackRate, targetFrame, loop, duration) ensure you compute targetFrame =
Math.max(0, currentFrame * playbackRate) (or equivalent) and then apply the loop
modulo or Math.min clipping, and ensure finalFrame (symbols: start, finalFrame)
cannot end up before start by preserving that clamp. This keeps defensive safety
for edge cases where relativeFrame may be negative.

Comment on lines +110 to 112
const RelativeFrameContext = createContext<number | null>(null);

export const useRelativeCurrentFrame = () => useContext(RelativeFrameContext);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of useRelativeCurrentFrame
rg 'useRelativeCurrentFrame' -t ts -t tsx --no-heading -n

Repository: codedbytahir/motionforge

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Find all usages of useRelativeCurrentFrame in .ts and .tsx files
rg 'useRelativeCurrentFrame' --glob '*.ts' --glob '*.tsx' -n

Repository: codedbytahir/motionforge

Length of output: 4363


🏁 Script executed:

#!/bin/bash
# Read Lottie.tsx around the usage site
sed -n '70,80p' src/lib/remotion/components/Lottie.tsx

Repository: codedbytahir/motionforge

Length of output: 475


Type definition mismatch: exported type declaration is outdated.

useRelativeCurrentFrame now returns number | null, but the exported type declaration in packages/motionforge/dist/index.d.ts:214 still declares the return type as number. This inconsistency will mislead external consumers. The Lottie component correctly handles the nullable return value with a null check, but the dist types need to be updated to reflect the actual implementation.

🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Sequence.tsx` around lines 110 - 112, The
exported type for useRelativeCurrentFrame is incorrect in the distributed types;
update the declaration so useRelativeCurrentFrame returns number | null to match
the implementation that uses RelativeFrameContext (createContext<number | null>)
and the hook useRelativeCurrentFrame; find the exported type/signature for
useRelativeCurrentFrame in the generated declaration file (the dist/index.d.ts
export for useRelativeCurrentFrame) and change its return type from number to
number | null so external consumers see the nullable return.

@codedbytahir codedbytahir merged commit 4e24e5e into main Feb 15, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant