Skip to content

refactor(flip-card): composable Front/Back API with motion-safe code#511

Open
sudhashrestha wants to merge 3 commits into
mainfrom
refactor/flip-card-composable-api
Open

refactor(flip-card): composable Front/Back API with motion-safe code#511
sudhashrestha wants to merge 3 commits into
mainfrom
refactor/flip-card-composable-api

Conversation

@sudhashrestha

@sudhashrestha sudhashrestha commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Replace fixed image/title props with FlipCard.Front and FlipCard.Back. Consumers own face markup. Flip uses transform-only transitions with reduced-motion guards.

Summary by CodeRabbit

  • New Features

    • FlipCard now uses explicit, composable front/back faces while keeping X/Y rotation control.
    • New FlippingCards grid component supports multi-card layouts via composable items and faces.
    • SplitReveal preloader is now composition-based with nested Images, Overlay, Shutter, Progress, and Task support (including external ready/progress control).
  • Documentation

    • Updated FlipCard, FlippingCards, and SplitReveal guides and added June 2026 changelog entries reflecting the composable APIs.
  • Storybook

    • Updated FlipCard, FlippingCards, and SplitReveal stories to render composed face/preloader structures instead of prop-driven content.

…forms

Replace fixed image/title props with FlipCard.Front and FlipCard.Back.
Consumers own face markup. Flip uses transform-only transitions with
reduced-motion guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sudhashrestha sudhashrestha requested a review from hari June 13, 2026 08:17
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

FlipCard refactored into composable subcomponents (FlipCardRoot, Front, Back) using React context for rotation axis; FlippingCards built as grid wrapper around FlipCard; SplitReveal modularized from 570-line monolith into composable submodules (root, overlay, progress, task, images, utilities) with Context-based state sharing; stories, demos, and documentation updated to show composition-based APIs; build-registry enhanced for robust relative import resolution.

Changes

Card Composition Refactor

Layer / File(s) Summary
FlipCard composition-based architecture
animata/card/flip-card.tsx
Core refactor introduces React context for rotation axis (FlipCardContext, useFlipCard), FlipCardRoot provider wrapping children, FlipCardFront and FlipCardBack face containers that consume context, and exports FlipCard via Object.assign composition.
FlipCard consumer migration
animata/card/swap-card.tsx, animata/card/flip-card.stories.tsx
SwapCard refactored to render two FlipCard instances with explicit FlipCard.Front (image + title overlay) and FlipCard.Back (styled description). Storybook Primary/Secondary stories converted from args-based props to custom render functions composing JSX.
FlippingCards grid component
animata/list/flipping-cards.tsx
New composable grid abstraction: FlippingCardsRoot (styled container), FlippingCardsItem (wraps FlipCard with sizing), getFlippingCardsAccent (HSL color helper), exported as FlippingCards with .Item subcomponent.
FlippingCards story and documentation
animata/list/flipping-cards.stories.tsx, content/docs/list/flipping-cards.mdx, content/docs/list/index.mdx
Story render function maps demo items to FlippingCards.Item/Front/Back with dynamic accent backgrounds. Docs show composable Item/Front/Back structure and co-install Flip Card + Marquee; Recent changes table added.
Card components documentation and changelog
content/docs/card/flip-card.mdx, content/docs/card/index.mdx, content/docs/changelog/2026-06.mdx
Added FlipCard Usage section with rotate axis and composable Front/Back examples; updated Recent changes; added 2026-06 changelog entries describing composable API and FlippingCards grid shift.

SplitReveal Modularization

Layer / File(s) Summary
SplitReveal types and context setup
animata/preloader/split-reveal/types.ts, animata/preloader/split-reveal/context.tsx
Define phase/progress/task types, context value interfaces, root props, and internal task registry; export useSplitReveal() and useSplitRevealInternal() hooks with provider validation.
SplitReveal root component and state management
animata/preloader/split-reveal/root.tsx
Implement SplitRevealRoot orchestrating preload→fade-ui→reveal→done lifecycle: reducer-based state, task registry with bootstrap scheduling, AbortController for stale-task cancellation, phase transitions with configurable timing, scroll locking, browser event handlers for cache restore and resume, context provision to children.
Overlay and shutter UI components
animata/preloader/split-reveal/overlay.tsx, animata/preloader/split-reveal/shutter.tsx
Implement SplitRevealOverlay (full-screen conditional overlay with phase-based pointer-events, CSS variables for timing/fade, accessibility attributes) and SplitRevealShutter (top/bottom half-height positioned divs consuming backgroundColor from context).
Progress tracking UI components
animata/preloader/split-reveal/progress-track.tsx, animata/preloader/split-reveal/progress-count.tsx, animata/preloader/split-reveal/progress-slot.tsx, animata/preloader/split-reveal/progress.tsx
Implement SplitRevealProgressTrack (1px animated bar), SplitRevealProgressCount (zero-padded counter), SplitRevealProgressSlot (centered wrapper), and SplitRevealProgress (composition root supporting render function or default UI).
Task system for async preload work
animata/preloader/split-reveal/execute-task.ts, animata/preloader/split-reveal/task.tsx
Implement executeTask to run task.run promises or consume task.generator async iterators with abort-aware progress reporting; implement SplitRevealTask component registering task definitions via refs and context callback.
Image preloading utility
animata/preloader/split-reveal/preload-images.ts, animata/preloader/split-reveal/images.tsx
Implement preloadImages to deduplicate and load URLs in parallel using img.decode() with load/error fallback; implement SplitRevealImages wrapping preload logic in a task reporting per-image progress.
Utility hooks for reduced motion and scroll locking
animata/preloader/split-reveal/use-prefers-reduced-motion.ts, animata/preloader/split-reveal/use-scroll-lock.ts
Implement usePrefersReducedMotion using useSyncExternalStore for browser prefers-reduced-motion setting; implement useScrollLock to disable scrolling while preserving scroll position and restoring on cleanup.
Public composition API and barrel exports
animata/preloader/split-reveal/index.tsx, animata/preloader/split-reveal.tsx
Build SplitReveal default export attaching Overlay/Shutter/Progress/Task/Images to SplitRevealRoot via Object.assign; export barrel module re-exporting all utilities, types, hooks, and components; replace monolithic split-reveal.tsx with lightweight barrel forward.
Story and demo updates
animata/preloader/split-reveal.stories.tsx, app/demo/library/hero/photographer-portfolio.tsx, app/demo/library/hero/photographer-portfolio-notes.tsx
Update Storybook story to compose SplitReveal with Images/Overlay/Shutter/Progress instead of inline args. Update photographer portfolio demo to remove custom renderProgress and use composed subcomponents. Update demo notes describing composition structure.
SplitReveal documentation and changelog
content/docs/preloader/split-reveal.mdx, content/docs/preloader/index.mdx, content/docs/changelog/2026-06.mdx
Rewrite docs showing composed SplitReveal with Images/Task/Overlay; document Task with run promises and async generators; document external ready/progress control; add customization and co-located import guidance; update 2026-06 changelog describing modularization.

Build System

Layer / File(s) Summary
Relative import resolution and export parsing
scripts/build-registry.js
Add resolveRelativeImport helper to resolve ./ imports to existing files with fallback chain (.tsx/.ts/index.tsx/index.ts); extend parseImports to capture both import and export ... from statements for complete dependency classification in bundling.

Sequence Diagrams

sequenceDiagram
  participant SplitRevealRoot
  participant SplitRevealTask
  participant executeTask
  participant report callback
  participant phases
  SplitRevealRoot->>SplitRevealRoot: initialize reducer (loading phase)
  SplitRevealTask->>SplitRevealRoot: registerTask(id, task)
  SplitRevealRoot->>executeTask: run all registered tasks with abort signal
  executeTask->>executeTask: run task.run or iterate task.generator
  executeTask->>report callback: report({loaded, total})
  report callback->>SplitRevealRoot: dispatch progress update
  SplitRevealRoot->>phases: transition fade-ui → reveal → done with delays
  SplitRevealRoot->>SplitRevealRoot: cleanup, invoke onComplete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • codse/animata#425: Both PRs modify animata/card/swap-card.tsx FlipCard wiring and refactor props handling for the card composition.
  • codse/animata#471: Overlaps on SplitReveal preloader refactoring and photographer portfolio demo wiring updates.
  • codse/animata#501: Shares SplitReveal modularization, composition API changes, and related documentation updates.

Suggested reviewers

  • hari

Poem

🐰 From props I hopped to context flows,
Front and Back in composition rose,
SplitReveal split into modular grace,
Task by task fills preload's space,
A carrot for composable APIs in place! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately reflects the main refactoring: converting FlipCard from a prop-based component to a composable Front/Back API and adding motion-safety features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/flip-card-composable-api

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying animata with  Cloudflare Pages  Cloudflare Pages

Latest commit: e2c3ac8
Status: ✅  Deploy successful!
Preview URL: https://5105e99d.animata.pages.dev
Branch Preview URL: https://refactor-flip-card-composabl.animata.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@animata/card/flip-card.tsx`:
- Around line 13-21: The hover rotation entries in ROTATION_CLASS (the hover
strings for keys x and y) cancel the flip under prefers-reduced-motion causing
FlipCardBack to remain pre-rotated and inaccessible; remove the motion-reduce
override tokens ("motion-reduce:group-hover/card:rotate-x-0" and
"motion-reduce:group-hover/card:rotate-y-0") from the hover values so the hover
still applies instantly for reduced-motion users (the existing
motion-reduce:transition-none handling at the transition site can remain as-is);
keep the back rotations ("rotate-x-180" / "rotate-y-180") unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae1301a5-1e28-4a41-8f14-241aa590eb0a

📥 Commits

Reviewing files that changed from the base of the PR and between ca96973 and 09d4bc8.

📒 Files selected for processing (6)
  • animata/card/flip-card.stories.tsx
  • animata/card/flip-card.tsx
  • animata/card/swap-card.tsx
  • content/docs/card/flip-card.mdx
  • content/docs/card/index.mdx
  • content/docs/changelog/2026-06.mdx

Comment on lines +13 to +21
const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
back: "rotate-y-180",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reduced-motion users can no longer access the back face.

Lines 15 and 19 cancel the hover rotation under prefers-reduced-motion, so the card never flips while FlipCardBack stays pre-rotated at 180°. That makes the back content visually unreachable for those users. motion-reduce:transition-none on Line 44 already removes the animation; the rotation override should be dropped so the flip still happens instantly.

Suggested fix
 const ROTATION_CLASS = {
   x: {
-    hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
+    hover: "group-hover/card:rotate-x-180",
     back: "rotate-x-180",
   },
   y: {
-    hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
+    hover: "group-hover/card:rotate-y-180",
     back: "rotate-y-180",
   },
 } as const;
📝 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
const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180 motion-reduce:group-hover/card:rotate-x-0",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180 motion-reduce:group-hover/card:rotate-y-0",
back: "rotate-y-180",
},
const ROTATION_CLASS = {
x: {
hover: "group-hover/card:rotate-x-180",
back: "rotate-x-180",
},
y: {
hover: "group-hover/card:rotate-y-180",
back: "rotate-y-180",
},
} as const;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/card/flip-card.tsx` around lines 13 - 21, The hover rotation entries
in ROTATION_CLASS (the hover strings for keys x and y) cancel the flip under
prefers-reduced-motion causing FlipCardBack to remain pre-rotated and
inaccessible; remove the motion-reduce override tokens
("motion-reduce:group-hover/card:rotate-x-0" and
"motion-reduce:group-hover/card:rotate-y-0") from the hover values so the hover
still applies instantly for reduced-motion users (the existing
motion-reduce:transition-none handling at the transition site can remain as-is);
keep the back rotations ("rotate-x-180" / "rotate-y-180") unchanged.

Replace list prop and inline 3D flip with FlippingCards.Item.Front/Back.
Storybook demo and docs updated; accent helper exported for back faces.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@animata/list/flipping-cards.stories.tsx`:
- Around line 63-67: The story hardcodes light-only colors in
FlippingCards.Item.Front and its children; make these classes theme-responsive
by replacing static color classes with light/dark-aware classes. Update
FlippingCards.Item.Front’s className from "flex bg-white" to include a dark
variant (e.g. "flex bg-white dark:bg-slate-900" or your design token like "flex
bg-surface dark:bg-surface-dark"), change the inner div’s border/text classes
from "border border-black/15 ... text-sm" to use dual-mode classes (e.g. "border
border-black/15 dark:border-white/15 text-black dark:text-white px-3 py-4
text-sm"), and update the span classes that use "text-black" and "border-black"
to corresponding "text-black dark:text-white" and "border-black/15
dark:border-white/15" (or your token equivalents) so the face adapts in dark
mode. Ensure all color uses within FlippingCards.Item.Front, the inner
container, and both spans are converted to light/dark variants or theme tokens.

In `@animata/list/flipping-cards.tsx`:
- Around line 31-33: Replace the arbitrary HSL hue math in
getFlippingCardsAccent with the theme accent token so the component uses the
shared accent styling; specifically, change the function
getFlippingCardsAccent(index: number) to return the theme value like
"hsl(var(--accent))" (or "hsl(var(--accent) / <alpha>)" if you need opacity)
instead of computing `(index * 47) % 360`.

In `@content/docs/list/flipping-cards.mdx`:
- Line 21: The docs note incorrectly lists Marquee as a required install for the
flipping-cards example; update content/docs/list/flipping-cards.mdx to remove
the sentence that instructs installing Marquee and instead mention only the
required Flip Card, and confirm the example file animata/list/flipping-cards.tsx
(which does not import or use Marquee) needs no code changes—just update the
text to say "This uses [Flip Card](/docs/card/flip-card) for the hover flip" and
remove the Marquee install reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9acbc95c-a295-48ea-8d5a-2f34ea4e7f5f

📥 Commits

Reviewing files that changed from the base of the PR and between 09d4bc8 and a273f76.

📒 Files selected for processing (5)
  • animata/list/flipping-cards.stories.tsx
  • animata/list/flipping-cards.tsx
  • content/docs/changelog/2026-06.mdx
  • content/docs/list/flipping-cards.mdx
  • content/docs/list/index.mdx

Comment on lines +63 to +67
<FlippingCards.Item.Front className="flex bg-white">
<div className="flex w-full flex-col border border-black/15 px-3 py-4 text-sm">
<span className="border-t-2 border-black pt-1 text-black">{item.font}</span>
<span className="mt-4 border-b-2 border-black px-1 font-serif text-8xl text-black">
{item.title}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the story face markup theme-responsive.

The face content hardcodes light-only colors (bg-white, text-black, border-black), so the demo does not adapt properly in dark mode.

Suggested token-based class update
-          <FlippingCards.Item.Front className="flex bg-white">
-            <div className="flex w-full flex-col border border-black/15 px-3 py-4 text-sm">
-              <span className="border-t-2 border-black pt-1 text-black">{item.font}</span>
-              <span className="mt-4 border-b-2 border-black px-1 font-serif text-8xl text-black">
+          <FlippingCards.Item.Front className="flex bg-card text-card-foreground">
+            <div className="flex w-full flex-col border border-border/60 px-3 py-4 text-sm">
+              <span className="border-t-2 border-border pt-1">{item.font}</span>
+              <span className="mt-4 border-b-2 border-border px-1 font-serif text-8xl">
                 {item.title}
               </span>
@@
-              <span className="text-black">See more</span>
-              <PlusCircle size={18} color="black" />
+              <span className="text-card-foreground">See more</span>
+              <PlusCircle size={18} className="text-card-foreground" />

As per coding guidelines, "All new components must be theme-responsive with support for both light and dark themes".

Also applies to: 84-85

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/list/flipping-cards.stories.tsx` around lines 63 - 67, The story
hardcodes light-only colors in FlippingCards.Item.Front and its children; make
these classes theme-responsive by replacing static color classes with
light/dark-aware classes. Update FlippingCards.Item.Front’s className from "flex
bg-white" to include a dark variant (e.g. "flex bg-white dark:bg-slate-900" or
your design token like "flex bg-surface dark:bg-surface-dark"), change the inner
div’s border/text classes from "border border-black/15 ... text-sm" to use
dual-mode classes (e.g. "border border-black/15 dark:border-white/15 text-black
dark:text-white px-3 py-4 text-sm"), and update the span classes that use
"text-black" and "border-black" to corresponding "text-black dark:text-white"
and "border-black/15 dark:border-white/15" (or your token equivalents) so the
face adapts in dark mode. Ensure all color uses within FlippingCards.Item.Front,
the inner container, and both spans are converted to light/dark variants or
theme tokens.

Source: Coding guidelines

Comment on lines +31 to 33
export function getFlippingCardsAccent(index: number) {
return `hsl(${(index * 47) % 360} 45% 55%)`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align getFlippingCardsAccent with the theme accent token.

The helper currently generates arbitrary hues, which bypasses the shared accent token and can drift from theme styling.

Suggested token-based implementation
 export function getFlippingCardsAccent(index: number) {
-  return `hsl(${(index * 47) % 360} 45% 55%)`;
+  const alpha = index % 2 === 0 ? "0.9" : "0.75";
+  return `hsl(var(--accent) / ${alpha})`;
 }

As per coding guidelines, "Theme accent color is hsl(var(--accent)) which is purple/violet".

📝 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
export function getFlippingCardsAccent(index: number) {
return `hsl(${(index * 47) % 360} 45% 55%)`;
}
export function getFlippingCardsAccent(index: number) {
const alpha = index % 2 === 0 ? "0.9" : "0.75";
return `hsl(var(--accent) / ${alpha})`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/list/flipping-cards.tsx` around lines 31 - 33, Replace the arbitrary
HSL hue math in getFlippingCardsAccent with the theme accent token so the
component uses the shared accent styling; specifically, change the function
getFlippingCardsAccent(index: number) to return the theme value like
"hsl(var(--accent))" (or "hsl(var(--accent) / <alpha>)" if you need opacity)
instead of computing `(index * 47) % 360`.

Source: Coding guidelines

```bash
npm install motion
```
This uses [Flip Card](/docs/card/flip-card) for the hover flip and [Marquee](/docs/container/marquee) for the back-face text. Install both by following their installation steps.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove Marquee from required install dependencies.

The current animata/list/flipping-cards.tsx implementation does not depend on Marquee, so this instruction is now misleading.

Suggested wording
-This uses [Flip Card](/docs/card/flip-card) for the hover flip and [Marquee](/docs/container/marquee) for the back-face text. Install both by following their installation steps.
+This uses [Flip Card](/docs/card/flip-card) for hover flip mechanics. Install it by following its installation steps.
📝 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
This uses [Flip Card](/docs/card/flip-card) for the hover flip and [Marquee](/docs/container/marquee) for the back-face text. Install both by following their installation steps.
This uses [Flip Card](/docs/card/flip-card) for hover flip mechanics. Install it by following its installation steps.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/docs/list/flipping-cards.mdx` at line 21, The docs note incorrectly
lists Marquee as a required install for the flipping-cards example; update
content/docs/list/flipping-cards.mdx to remove the sentence that instructs
installing Marquee and instead mention only the required Flip Card, and confirm
the example file animata/list/flipping-cards.tsx (which does not import or use
Marquee) needs no code changes—just update the text to say "This uses [Flip
Card](/docs/card/flip-card) for the hover flip" and remove the Marquee install
reference.

Opt-in Images and Task primitives, external ready/progress control, and
tree-shakeable modules. Registry bundler follows co-located imports; photographer
demo uses the default Progress UI.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (1)
animata/preloader/split-reveal/index.tsx (1)

3-3: ⚡ Quick win

Use a co-located stylesheet for this TSX entrypoint.

index.tsx imports ../split-reveal.css; this breaks the co-location rule for animata/**/*.{tsx,css}. Please move/rename to a co-located file (for example ./index.css) and import it locally.

As per coding guidelines, animata/**/*.{tsx,css}: “Create co-located <name>.css files for keyframes, pseudo-elements, and selectors that Tailwind cannot express, and import them from the corresponding TSX file.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/index.tsx` at line 3, The index.tsx file
imports a non-co-located stylesheet at ../split-reveal.css, which violates the
co-location rule for animata components. Move the split-reveal.css file to the
same directory as index.tsx and rename it to index.css, then update the import
statement in index.tsx to reference the co-located file with a relative path
like ./index.css instead of ../split-reveal.css.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@animata/preloader/split-reveal/execute-task.ts`:
- Around line 15-27: The abort handling in the generator execution loop has two
issues: first, iterator.next() is called at the start of each loop iteration
before checking signal.aborted, allowing one extra generator step to execute
even when aborted; second, when returning due to abort (when signal.aborted is
true), the code returns without calling iterator.return(), which skips generator
cleanup such as finally blocks. To fix this, move the abort check
(signal.aborted) to occur before calling iterator.next() in the while loop
condition or at the loop start, and when returning due to abort, call
iterator.return() first to ensure all cleanup code in the generator executes
properly.

In `@animata/preloader/split-reveal/overlay.tsx`:
- Around line 23-35: The overlay component spreads the consumer-provided props
after defining the required style, which allows a consumer-provided style prop
to override the critical overlay styles (zIndex, --split-reveal-duration,
--split-reveal-progress-fade). Move the style prop definition to come after the
{...props} spread so that the required internal styles take precedence and
cannot be overridden by consumer props, ensuring the overlay behavior is not
broken.

In `@animata/preloader/split-reveal/progress-count.tsx`:
- Around line 20-24: The inline styles in the progress-count component use
hex-suffix opacity notation (${foregroundColor}73 and ${foregroundColor}33)
which only works with hex color strings and breaks with hsl(), rgb(), or CSS
variable values. Replace these color assignments with a format that works
universally across all color types, such as using the rgba() function with a
calculated alpha value or applying opacity through a separate CSS property that
works regardless of the foregroundColor format.

In `@animata/preloader/split-reveal/root.tsx`:
- Around line 21-22: The backgroundColor and foregroundColor default parameters
in the SplitReveal component are hard-coded to light theme values (`#fff` and
`#000`), which breaks the component's appearance in dark mode. Replace these
hard-coded color defaults with theme-responsive tokens that automatically adapt
to the current theme context, ensuring the component works correctly for both
light and dark themes as required by the coding guidelines.
- Around line 121-124: The useEffect hook that manages task bootstrap is
including `controlledReady` and `controlledProgress` in its dependency list,
which causes it to re-run and abort in-flight executeTask calls whenever these
props change. Remove `controlledReady` and `controlledProgress` from the
dependency array of the useEffect that contains the task bootstrap logic. The
effect should initialize the task once at mount, and changing the `ready` flag
should only unblock the reveal without restarting the actual task work defined
in the SplitRevealTask.
- Around line 237-248: The Promise.all() chain for executing tasks only calls
the completion logic in the .then() handler, which means if any task rejects,
tasksDone never gets set to true, phase remains "loading", and the page stays
frozen in a deadlock state. Add error handling to the Promise chain by attaching
a .catch() or .finally() block that ensures the same completion logic (setting
tasksDone = true and calling maybeReveal(currentRun)) executes regardless of
whether the tasks succeed or fail. Make sure the abort signal check (currentRun
!== runId || abortController?.signal.aborted) is applied in both the success and
failure paths to prevent race conditions.

In `@animata/preloader/split-reveal/task.tsx`:
- Around line 25-36: The useEffect dependency array in the task registration is
incomplete. The effect creates a task definition using runRef.current and
generatorRef.current, but the dependency array only includes id and
registerTask. When the run or generator props change, their ref values update
but the effect does not re-run, causing the registered task to have stale
generator and run values. Add runRef and generatorRef (or their current values
if they are stable) to the dependency array so the task gets re-registered
whenever these values change.

In `@animata/preloader/split-reveal/use-prefers-reduced-motion.ts`:
- Around line 5-13: The subscribeReducedMotion function uses addEventListener
and removeEventListener on the MediaQueryList object, but these methods are not
supported in older browsers. Add a feature check to detect whether
addEventListener exists on the MediaQueryList object returned by
window.matchMedia. If it does not exist, fall back to using the deprecated
addListener method for adding the listener, and removeListener for removing it
in the cleanup function. This will ensure compatibility with older browser
versions while maintaining modern browser behavior.

In `@scripts/build-registry.js`:
- Around line 216-218: Relative paths with `../../` sequences can normalize
outside the project root and still be read and bundled into the registry JSON,
leaking unintended local files. After normalizing the path using
path.posix.normalize in the path resolution logic around line 216, add a
validation check to ensure the fully resolved absolute path (when joined with
ROOT) stays within the project root directory before returning it as a valid
base. The same constraint should be enforced at the bundling site around lines
256-259 to prevent any escaped paths from being included in the generated
registry output.
- Around line 267-268: The regex pattern in the `re` variable at lines 267-268
does not match TypeScript type re-exports (`export type { ... } from`). Update
the regex to include `type\s+` as an optional pattern in the export alternatives
so that both `export { ... } from` and `export type { ... } from` syntax are
matched and properly captured during bundling/classification.

---

Nitpick comments:
In `@animata/preloader/split-reveal/index.tsx`:
- Line 3: The index.tsx file imports a non-co-located stylesheet at
../split-reveal.css, which violates the co-location rule for animata components.
Move the split-reveal.css file to the same directory as index.tsx and rename it
to index.css, then update the import statement in index.tsx to reference the
co-located file with a relative path like ./index.css instead of
../split-reveal.css.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fbfaae16-71a4-4d95-88ef-4390cc4fb72d

📥 Commits

Reviewing files that changed from the base of the PR and between a273f76 and e2c3ac8.

⛔ Files ignored due to path filters (1)
  • app/demo/generated/demo-sources.ts is excluded by !**/generated/**
📒 Files selected for processing (24)
  • animata/preloader/split-reveal.stories.tsx
  • animata/preloader/split-reveal.tsx
  • animata/preloader/split-reveal/context.tsx
  • animata/preloader/split-reveal/execute-task.ts
  • animata/preloader/split-reveal/images.tsx
  • animata/preloader/split-reveal/index.tsx
  • animata/preloader/split-reveal/overlay.tsx
  • animata/preloader/split-reveal/preload-images.ts
  • animata/preloader/split-reveal/progress-count.tsx
  • animata/preloader/split-reveal/progress-slot.tsx
  • animata/preloader/split-reveal/progress-track.tsx
  • animata/preloader/split-reveal/progress.tsx
  • animata/preloader/split-reveal/root.tsx
  • animata/preloader/split-reveal/shutter.tsx
  • animata/preloader/split-reveal/task.tsx
  • animata/preloader/split-reveal/types.ts
  • animata/preloader/split-reveal/use-prefers-reduced-motion.ts
  • animata/preloader/split-reveal/use-scroll-lock.ts
  • app/demo/library/hero/photographer-portfolio-notes.tsx
  • app/demo/library/hero/photographer-portfolio.tsx
  • content/docs/changelog/2026-06.mdx
  • content/docs/preloader/index.mdx
  • content/docs/preloader/split-reveal.mdx
  • scripts/build-registry.js
✅ Files skipped from review due to trivial changes (5)
  • animata/preloader/split-reveal/progress-slot.tsx
  • animata/preloader/split-reveal/progress-track.tsx
  • animata/preloader/split-reveal/types.ts
  • app/demo/library/hero/photographer-portfolio-notes.tsx
  • content/docs/changelog/2026-06.mdx

Comment on lines +15 to +27
if (task.generator) {
const iterator = task.generator(ctx);
let result = await iterator.next();

while (!result.done) {
if (signal.aborted) {
return;
}
if (result.value) {
report(result.value);
}
result = await iterator.next();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Abort handling pulls one extra generator step and skips iterator cleanup.

At Line 17, iterator.next() runs before abort is checked, so aborted tasks can still execute work once. At Line 21, returning without iterator.return() can bypass generator cleanup (finally blocks).

Suggested fix
   if (task.generator) {
+    if (signal.aborted) {
+      return;
+    }
     const iterator = task.generator(ctx);
-    let result = await iterator.next();
+    let result = await iterator.next();

     while (!result.done) {
       if (signal.aborted) {
+        await iterator.return?.();
         return;
       }
       if (result.value) {
         report(result.value);
       }
       result = await iterator.next();
     }
   }
📝 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
if (task.generator) {
const iterator = task.generator(ctx);
let result = await iterator.next();
while (!result.done) {
if (signal.aborted) {
return;
}
if (result.value) {
report(result.value);
}
result = await iterator.next();
}
if (task.generator) {
if (signal.aborted) {
return;
}
const iterator = task.generator(ctx);
let result = await iterator.next();
while (!result.done) {
if (signal.aborted) {
await iterator.return?.();
return;
}
if (result.value) {
report(result.value);
}
result = await iterator.next();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/execute-task.ts` around lines 15 - 27, The
abort handling in the generator execution loop has two issues: first,
iterator.next() is called at the start of each loop iteration before checking
signal.aborted, allowing one extra generator step to execute even when aborted;
second, when returning due to abort (when signal.aborted is true), the code
returns without calling iterator.return(), which skips generator cleanup such as
finally blocks. To fix this, move the abort check (signal.aborted) to occur
before calling iterator.next() in the while loop condition or at the loop start,
and when returning due to abort, call iterator.return() first to ensure all
cleanup code in the generator executes properly.

Comment on lines +23 to +35
style={
{
zIndex,
"--split-reveal-duration": `${revealDuration}s`,
"--split-reveal-progress-fade": `${progressFadeMs}ms`,
} as CSSProperties
}
data-phase={phase}
aria-busy={phase === "loading"}
aria-live="polite"
data-split-reveal-overlay=""
{...props}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve internal overlay styles when forwarding style props.

...props is spread after style, so a consumer-provided style can replace required values (zIndex, --split-reveal-*) and break the overlay behavior.

Proposed fix
-export function SplitRevealOverlay({ className, children, ...props }: ComponentProps<"div">) {
+export function SplitRevealOverlay({
+  className,
+  children,
+  style,
+  ...props
+}: ComponentProps<"div">) {
@@
       style={
         {
+          ...style,
           zIndex,
           "--split-reveal-duration": `${revealDuration}s`,
           "--split-reveal-progress-fade": `${progressFadeMs}ms`,
         } as CSSProperties
       }
📝 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
style={
{
zIndex,
"--split-reveal-duration": `${revealDuration}s`,
"--split-reveal-progress-fade": `${progressFadeMs}ms`,
} as CSSProperties
}
data-phase={phase}
aria-busy={phase === "loading"}
aria-live="polite"
data-split-reveal-overlay=""
{...props}
>
export function SplitRevealOverlay({
className,
children,
style,
...props
}: ComponentProps<"div">) {
// ... other code ...
return (
<div
className={cn("relative", className)}
ref={overlayRef}
style={
{
...style,
zIndex,
"--split-reveal-duration": `${revealDuration}s`,
"--split-reveal-progress-fade": `${progressFadeMs}ms`,
} as CSSProperties
}
data-phase={phase}
aria-busy={phase === "loading"}
aria-live="polite"
data-split-reveal-overlay=""
{...props}
>
{children}
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/overlay.tsx` around lines 23 - 35, The overlay
component spreads the consumer-provided props after defining the required style,
which allows a consumer-provided style prop to override the critical overlay
styles (zIndex, --split-reveal-duration, --split-reveal-progress-fade). Move the
style prop definition to come after the {...props} spread so that the required
internal styles take precedence and cannot be overridden by consumer props,
ensuring the overlay behavior is not broken.

Comment on lines +20 to +24
style={{ color: `${foregroundColor}73` }}
>
{String(loaded).padStart(2, "0")}
<span style={{ color: `${foregroundColor}33` }}> / </span>
{String(total).padStart(2, "0")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hex-suffix opacity on arbitrary color strings.

${foregroundColor}73 / ${foregroundColor}33 only works for hex colors. For hsl(...)/rgb(...)/CSS vars, this produces invalid color values.

Proposed fix
-    <p
+    <p
       className={cn(
         "mt-3 text-center text-[11px] font-medium tabular-nums tracking-[0.12em]",
         className,
       )}
-      style={{ color: `${foregroundColor}73` }}
     >
-      {String(loaded).padStart(2, "0")}
-      <span style={{ color: `${foregroundColor}33` }}> / </span>
-      {String(total).padStart(2, "0")}
+      <span style={{ color: foregroundColor, opacity: 0.45 }}>
+        {String(loaded).padStart(2, "0")}
+      </span>
+      <span style={{ color: foregroundColor, opacity: 0.2 }}> / </span>
+      <span style={{ color: foregroundColor, opacity: 0.45 }}>
+        {String(total).padStart(2, "0")}
+      </span>
     </p>
📝 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
style={{ color: `${foregroundColor}73` }}
>
{String(loaded).padStart(2, "0")}
<span style={{ color: `${foregroundColor}33` }}> / </span>
{String(total).padStart(2, "0")}
<p
className={cn(
"mt-3 text-center text-[11px] font-medium tabular-nums tracking-[0.12em]",
className,
)}
>
<span style={{ color: foregroundColor, opacity: 0.45 }}>
{String(loaded).padStart(2, "0")}
</span>
<span style={{ color: foregroundColor, opacity: 0.2 }}> / </span>
<span style={{ color: foregroundColor, opacity: 0.45 }}>
{String(total).padStart(2, "0")}
</span>
</p>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/progress-count.tsx` around lines 20 - 24, The
inline styles in the progress-count component use hex-suffix opacity notation
(${foregroundColor}73 and ${foregroundColor}33) which only works with hex color
strings and breaks with hsl(), rgb(), or CSS variable values. Replace these
color assignments with a format that works universally across all color types,
such as using the rgba() function with a calculated alpha value or applying
opacity through a separate CSS property that works regardless of the
foregroundColor format.

Comment on lines +21 to +22
backgroundColor = "#fff",
foregroundColor = "#000",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use theme-aware defaults for the overlay colors.

These defaults hard-code the light theme, so dark-mode pages get a white overlay and black foreground unless every caller overrides them. Default to the existing theme tokens instead of #fff / #000.

As per coding guidelines, animata/**/*.tsx: “All new components must be theme-responsive with support for both light and dark themes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/root.tsx` around lines 21 - 22, The
backgroundColor and foregroundColor default parameters in the SplitReveal
component are hard-coded to light theme values (`#fff` and `#000`), which breaks the
component's appearance in dark mode. Replace these hard-coded color defaults
with theme-responsive tokens that automatically adapt to the current theme
context, ensuring the component works correctly for both light and dark themes
as required by the coding guidelines.

Source: Coding guidelines

Comment on lines +121 to +124
useEffect(() => {
if (phaseRef.current === "done") {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't restart task execution when ready or controlled progress changes.

This effect owns the task bootstrap, so including controlledReady and controlledProgress in its dependency list aborts in-flight executeTask(...) runs and starts them over on every prop change. With registered tasks, flipping ready from false to true should only unblock reveal; it should not relaunch the preload work defined by SplitRevealTask in animata/preloader/split-reveal/task.tsx:17-39.

Also applies to: 260-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/root.tsx` around lines 121 - 124, The
useEffect hook that manages task bootstrap is including `controlledReady` and
`controlledProgress` in its dependency list, which causes it to re-run and abort
in-flight executeTask calls whenever these props change. Remove
`controlledReady` and `controlledProgress` from the dependency array of the
useEffect that contains the task bootstrap logic. The effect should initialize
the task once at mount, and changing the `ready` flag should only unblock the
reveal without restarting the actual task work defined in the SplitRevealTask.

Comment on lines +237 to +248
Promise.all(
tasks.map((task) =>
executeTask(task, report, abortController?.signal ?? new AbortSignal()),
),
).then(() => {
if (currentRun !== runId || abortController?.signal.aborted) {
return;
}

tasksDone = true;
maybeReveal(currentRun);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle task failures so the preloader can't deadlock.

If any registered task rejects, this chain never marks tasksDone, phase stays "loading", and useScrollLock() keeps the page frozen. Add a rejection path here so the component completes deterministically even when preload work fails.

Suggested fix
-      Promise.all(
+      Promise.allSettled(
         tasks.map((task) =>
-          executeTask(task, report, abortController?.signal ?? new AbortSignal()),
+          executeTask(task, report, abortController.signal),
         ),
-      ).then(() => {
-        if (currentRun !== runId || abortController?.signal.aborted) {
+      ).then((results) => {
+        if (currentRun !== runId || abortController.signal.aborted) {
           return;
         }
 
+        const failed = results.some((result) => result.status === "rejected");
+        if (failed) {
+          // TODO: surface the failure via logging or an error callback.
+        }
+
         tasksDone = true;
         maybeReveal(currentRun);
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/root.tsx` around lines 237 - 248, The
Promise.all() chain for executing tasks only calls the completion logic in the
.then() handler, which means if any task rejects, tasksDone never gets set to
true, phase remains "loading", and the page stays frozen in a deadlock state.
Add error handling to the Promise chain by attaching a .catch() or .finally()
block that ensures the same completion logic (setting tasksDone = true and
calling maybeReveal(currentRun)) executes regardless of whether the tasks
succeed or fail. Make sure the abort signal check (currentRun !== runId ||
abortController?.signal.aborted) is applied in both the success and failure
paths to prevent race conditions.

Comment on lines +25 to +36
useEffect(() => {
const task: SplitRevealTaskDefinition = {
run: runRef.current ? (ctx) => runRef.current?.(ctx) ?? Promise.resolve() : undefined,
generator: generatorRef.current,
};

if (!task.run && !task.generator) {
return;
}

return registerTask(id, task);
}, [id, registerTask]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Task registration can drift from latest generator/run props.

Because the effect only depends on id and registerTask, updates to run/generator do not re-register the task definition; generator is especially stale since it is captured directly at registration time.

Suggested fix
 export function SplitRevealTask({ run, generator }: SplitRevealTaskProps) {
   const id = useId();
   const { registerTask } = useSplitRevealInternal();
-  const runRef = useRef(run);
-  const generatorRef = useRef(generator);
-  runRef.current = run;
-  generatorRef.current = generator;

   useEffect(() => {
     const task: SplitRevealTaskDefinition = {
-      run: runRef.current ? (ctx) => runRef.current?.(ctx) ?? Promise.resolve() : undefined,
-      generator: generatorRef.current,
+      run,
+      generator,
     };

     if (!task.run && !task.generator) {
       return;
     }

     return registerTask(id, task);
-  }, [id, registerTask]);
+  }, [id, registerTask, run, generator]);

   return null;
 }
📝 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
useEffect(() => {
const task: SplitRevealTaskDefinition = {
run: runRef.current ? (ctx) => runRef.current?.(ctx) ?? Promise.resolve() : undefined,
generator: generatorRef.current,
};
if (!task.run && !task.generator) {
return;
}
return registerTask(id, task);
}, [id, registerTask]);
export function SplitRevealTask({ run, generator }: SplitRevealTaskProps) {
const id = useId();
const { registerTask } = useSplitRevealInternal();
useEffect(() => {
const task: SplitRevealTaskDefinition = {
run,
generator,
};
if (!task.run && !task.generator) {
return;
}
return registerTask(id, task);
}, [id, registerTask, run, generator]);
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/task.tsx` around lines 25 - 36, The useEffect
dependency array in the task registration is incomplete. The effect creates a
task definition using runRef.current and generatorRef.current, but the
dependency array only includes id and registerTask. When the run or generator
props change, their ref values update but the effect does not re-run, causing
the registered task to have stale generator and run values. Add runRef and
generatorRef (or their current values if they are stable) to the dependency
array so the task gets re-registered whenever these values change.

Comment on lines +5 to +13
function subscribeReducedMotion(callback: () => void) {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
mq.addEventListener("change", callback);
return () => mq.removeEventListener("change", callback);
}

function getReducedMotionSnapshot() {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

Copy link
Copy Markdown

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

🌐 Web query:

What is the current browser support for MediaQueryList.addEventListener("change") compared with addListener/removeListener, and what compatibility fallback is recommended for matchMedia subscriptions?

💡 Result:

As of June 2026, the modern and recommended approach for observing media query changes is to use MediaQueryList.addEventListener('change', callback) [1][2]. This method utilizes the standard EventTarget interface, which MediaQueryList now implements in all modern browsers [3][4]. The older methods, addListener() and removeListener(), are deprecated [1][5]. They were originally provided as an alias to addEventListener() for backward compatibility before MediaQueryList inherited from EventTarget [1][6]. While they remain available in many browsers for legacy support, they should be avoided in new development [1][4]. Recommended Compatibility Fallback To ensure your application remains compatible with older environments (such as older versions of Safari that did not support addEventListener on MediaQueryList objects), you should perform a feature check before subscribing to media query changes [2][7][4]. You can implement a robust subscription pattern as follows: const mediaQuery = window.matchMedia('(min-width: 768px)'); const handler = (event) => { // Handle the change console.log('Matches:', event.matches); }; // Compatibility check if (mediaQuery.addEventListener) { mediaQuery.addEventListener('change', handler); } else { // Legacy fallback mediaQuery.addListener(handler); } When cleaning up or removing the subscription, apply the same logic: if (mediaQuery.removeEventListener) { mediaQuery.removeEventListener('change', handler); } else { mediaQuery.removeListener(handler); } This pattern safely defaults to the modern standard while providing a reliable fallback for legacy browser environments [4][8].

Citations:


Provide addEventListener fallback for older browser compatibility.

MediaQueryList.addEventListener support varies across legacy browsers (e.g., older Safari). Add a feature check to fall back to the deprecated addListener method for wider compatibility.

Suggested patch
 function subscribeReducedMotion(callback: () => void) {
   const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
-  mq.addEventListener("change", callback);
-  return () => mq.removeEventListener("change", callback);
+  if ("addEventListener" in mq) {
+    mq.addEventListener("change", callback);
+    return () => mq.removeEventListener("change", callback);
+  }
+  mq.addListener(callback);
+  return () => mq.removeListener(callback);
 }
 
 function getReducedMotionSnapshot() {
-  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
+  return "addEventListener" in mq ? mq.matches : false;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@animata/preloader/split-reveal/use-prefers-reduced-motion.ts` around lines 5
- 13, The subscribeReducedMotion function uses addEventListener and
removeEventListener on the MediaQueryList object, but these methods are not
supported in older browsers. Add a feature check to detect whether
addEventListener exists on the MediaQueryList object returned by
window.matchMedia. If it does not exist, fall back to using the deprecated
addListener method for adding the listener, and removeListener for removing it
in the cleanup function. This will ensure compatibility with older browser
versions while maintaining modern browser behavior.

Comment thread scripts/build-registry.js
Comment on lines +216 to +218
const base = path.posix.normalize(path.posix.join(dir, spec));
if (path.extname(base)) {
return readIfExists(path.join(ROOT, base)) ? base : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain resolved relative paths to stay under project root.

At Line 216 and Line 218, ./../../... imports can normalize outside the repository and still be read, then bundled at Line 256-259. This can leak unintended local files into generated registry JSON.

Suggested fix
 function resolveRelativeImport(dir, spec) {
   const base = path.posix.normalize(path.posix.join(dir, spec));
+  const absBase = path.resolve(ROOT, base);
+  const relToRoot = path.relative(ROOT, absBase);
+  if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
+    return null;
+  }
   if (path.extname(base)) {
-    return readIfExists(path.join(ROOT, base)) ? base : null;
+    return readIfExists(absBase) ? base : null;
   }
   for (const ext of [".tsx", ".ts"]) {
     const candidate = `${base}${ext}`;
-    if (readIfExists(path.join(ROOT, candidate))) {
+    if (readIfExists(path.join(ROOT, candidate))) {
       return candidate;
     }
   }

Also applies to: 256-259

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-registry.js` around lines 216 - 218, Relative paths with
`../../` sequences can normalize outside the project root and still be read and
bundled into the registry JSON, leaking unintended local files. After
normalizing the path using path.posix.normalize in the path resolution logic
around line 216, add a validation check to ensure the fully resolved absolute
path (when joined with ROOT) stays within the project root directory before
returning it as a valid base. The same constraint should be enforced at the
bundling site around lines 256-259 to prevent any escaped paths from being
included in the generated registry output.

Comment thread scripts/build-registry.js
Comment on lines +267 to +268
const re =
/(?:import\s+(?:[^'"]*?\s+from\s+)?|export\s+(?:\*|\{[^}]*\})\s+from\s+)["']([^"']+)["']/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether the repo uses TS `export type ... from` syntax that current regex won't match.
# Expected: if matches exist, parser should be extended.

set -euo pipefail

echo "1) Find candidate source lines:"
rg -nP --type=ts --type=tsx '\bexport\s+type\s*\{[^}]*\}\s+from\s+["'\''][^"'\'']+["'\'']' animata scripts content || true

echo
echo "2) Show current parseImports regex:"
rg -n 'const re =|export\\s+\\(?:\\*|\\{\\[\\^\\}\\]\\*\\}\\)' scripts/build-registry.js -n -A3 -B2 || true

Repository: codse/animata

Length of output: 342


🏁 Script executed:

# First, examine the actual regex at lines 267-268
sed -n '265,270p' scripts/build-registry.js

Repository: codse/animata

Length of output: 285


🏁 Script executed:

# Search for 'export type { ... } from' patterns in TypeScript files (simpler pattern)
rg -n 'export\s+type\s+\{[^}]*\}\s+from' --type ts

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Also check without whitespace restrictions to see if pattern exists at all
rg -n 'export.*type.*\{.*\}.*from' --type ts | head -20

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Search more broadly across all file types for export type patterns
rg 'export\s+type\s+\{' --multiline | head -20

Repository: codse/animata

Length of output: 112


🏁 Script executed:

# Also check if export type exists at all in the repo (any variant)
rg 'export\s+type' | wc -l

Repository: codse/animata

Length of output: 60


🏁 Script executed:

# Show a few examples if export type exists (to understand usage)
rg 'export\s+type' | head -5

Repository: codse/animata

Length of output: 318


🏁 Script executed:

# Search specifically for 'export type { ... } from' re-export pattern
rg 'export\s+type\s+\{[^}]*\}\s+from' --multiline

Repository: codse/animata

Length of output: 849


🏁 Script executed:

# Show the split-reveal example that was found
rg -A2 'export type \{' animata/preloader/split-reveal/index.tsx

Repository: codse/animata

Length of output: 116


🏁 Script executed:

# Check if the current regex would catch export type { ... } from patterns
# Test with a simple Node script
node << 'EOF'
const re = /(?:import\s+(?:[^'"]*?\s+from\s+)?|export\s+(?:\*|\{[^}]*\})\s+from\s+)["']([^"']+)["']/g;

// Test cases
const testCases = [
  'export { Foo } from "./types"',
  'export type { Foo } from "./types"',
  'export * from "./module"',
  'export type * from "./module"',
  'import { Foo } from "./types"',
  'import type { Foo } from "./types"',
];

testCases.forEach(testCase => {
  const match = re.test(testCase);
  re.lastIndex = 0; // Reset regex
  console.log(`"${testCase}" → ${match ? 'MATCH' : 'NO MATCH'}`);
});
EOF

Repository: codse/animata

Length of output: 308


Include export type { ... } from in import parsing.

The regex at line 268 misses TypeScript type re-exports (export type { Foo } from "./types"). This pattern exists in the codebase (e.g., animata/preloader/split-reveal/index.tsx) and will be silently skipped during bundling/classification. While import type is already handled, the corresponding export type variants are not.

Update the regex to include type\s+ in the export alternatives to match both export { ... } from and export type { ... } from syntax.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-registry.js` around lines 267 - 268, The regex pattern in the
`re` variable at lines 267-268 does not match TypeScript type re-exports
(`export type { ... } from`). Update the regex to include `type\s+` as an
optional pattern in the export alternatives so that both `export { ... } from`
and `export type { ... } from` syntax are matched and properly captured during
bundling/classification.

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