Skip to content

feat(icons): add animated brand icons for OpenAI, Gemini, Grok, Qwen,…#73

Merged
Abhijit-Jha merged 3 commits intomasterfrom
feat/icons
Jan 11, 2026
Merged

feat(icons): add animated brand icons for OpenAI, Gemini, Grok, Qwen,…#73
Abhijit-Jha merged 3 commits intomasterfrom
feat/icons

Conversation

@Abhijit-Jha
Copy link
Copy Markdown
Member

@Abhijit-Jha Abhijit-Jha commented Jan 11, 2026

feat(icons): add animated brand icons for major tech platforms

Added the following brand icons:

  • AI: OpenAI, Gemini, Grok, Qwen
  • Dev Tools: Vercel, Cursor, Railway, AWS
  • Frameworks: React, React Native
  • Social/Comm: Threads, Telegram, Zoom
  • Productivity/Payments: Notion, Stripe, PayPal
  • OS: Windows

Summary by CodeRabbit

  • New Features
    • Added many new animated brand icons (AWS, PayPal, OpenAI, React, React Native, Stripe, Telegram, Threads, Windows, Zoom, Rails/Railway, Cursor, Gemini, Grok, Notion, Qwen, Vercel and others).
  • Chores
    • Updated package branding to "itshover".
    • Removed the sticky banner from the app layout.
  • Refactor
    • Renamed existing icons to use a consistent "brand-" prefix and updated the public icon registry.

✏️ Tip: You can customize this high-level summary in your review settings.

… Vercel, Cursor, Railway, AWS, React, React Native, Threads, Telegram, Zoom, Notion, Stripe, PayPal, and Windows
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Jan 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
itshover-icons Ready Ready Preview, Comment Jan 11, 2026 4:55pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 11, 2026

📝 Walkthrough

Walkthrough

Removed StickyBanner from RootLayout; added many brand-* animated icon components (with startAnimation/stopAnimation handles), renamed several icons to brand-* variants, updated icon registries/exports and icon name lists, and changed package metadata to "itshover".

Changes

Cohort / File(s) Summary
Layout & Package
app/layout.tsx, package.json
Removed StickyBanner import and JSX from RootLayout; updated package name/description.
New Brand Icons
icons/brand-aws-icon.tsx, icons/brand-openai-icon.tsx, icons/brand-paypal-icon.tsx, icons/brand-react-icon.tsx, icons/brand-react-native-icon.tsx, icons/brand-stripe-icon.tsx, icons/brand-telegram-icon.tsx, icons/brand-threads-icon.tsx, icons/brand-windows-icon.tsx, icons/brand-zoom-icon.tsx
Added 10 new forwardRef animated icon components. Each uses motion/react/useAnimate and exposes startAnimation/stopAnimation via useImperativeHandle.
Renamed / Brand-prefixed Icons
icons/brand-cursor-icon.tsx, icons/brand-gemini-icon.tsx, icons/brand-grok-icon.tsx, icons/brand-notion-icon.tsx, icons/brand-qwen-icon.tsx, icons/brand-railway-icon.tsx, icons/brand-vercel-icon.tsx
Renamed components and default exports to Brand* variants, updated displayName, and added default CSS class augmentations; some updated animation logic (railway/vercel).
Existing Icons — Animation Control Changes
icons/*.tsx (accessibility, alarm-clock-plus, ambulance, docker, globe, mouse-pointer-2, qrcode, spotify, vinyl, washing-machine, etc.)
Reworked many icon implementations to use useRef animationControls arrays, added stopAnimation to imperative handles, and replaced useCallback wrappers with plain start/stop functions.
Icon Registry & Index
icons/index.ts, lib/icon-names.ts, lib/icons.ts
Updated ICON_LIST/ICONS/ICON_NAMES: renamed entries to brand-* variants, added many new brand-* entries, removed several non-brand entries, and exported the new set.
Registry Files (root & public)
registry.json, public/r/registry.json, public/r/brand-*.json (many files)
Added registry entries and public/r/* items for all new brand-* icons and their icons/types.ts; removed corresponding old non-brand entries; synchronized files mapping and dependencies.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped along the registry trail,
Found brand-new icons, bright and hale.
I nudged a smile, I gave a spin—
Start and stop, the dances begin.
StickyBanner gone; it’s time to play—
itshover hops and leads the way! 🎨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding animated brand icons for multiple platforms (OpenAI, Gemini, Grok, Qwen, etc.), which aligns with the substantial additions across 17 new brand icon files and supporting infrastructure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 11, 2026

Greptile Overview

Greptile Summary

This PR adds 17 new animated brand icons (OpenAI, Gemini, Grok, Qwen, AWS, Cursor, Railway, Vercel, React, React Native, Threads, Telegram, Zoom, Notion, Stripe, PayPal, Windows) and renames 7 existing icons to follow a consistent brand-* naming convention.

Key Changes

  • New icons: 10 brand new animated icon components for major platforms
  • Renamed icons: 7 icons renamed from inconsistent names (e.g., cursor-ide-icon, railway-icon, vercel-icon) to brand-* prefix
  • Registry updates: All icon registry files and metadata updated accordingly
  • Package metadata: Updated package name from animated-icons to itshover
  • Banner removal: Removed promotional banner for evilcharts.com from layout

Issues Found

  • icons/brand-railway-icon.tsx: Incorrect useImperativeHandle dependency array [start, stop] will cause unnecessary re-renders since these functions aren't memoized
  • icons/brand-vercel-icon.tsx: Missing dependency array in useImperativeHandle (minor issue, but inconsistent with the use of useCallback)

Notes

  • All new icon implementations follow the existing pattern with motion/react for animations
  • Icons properly implement the AnimatedIconHandle interface with startAnimation and stopAnimation methods
  • Registry files are auto-generated and correctly include all new icons

Confidence Score: 4/5

  • Safe to merge with one bug fix needed in brand-railway-icon.tsx
  • Score reflects one critical React hook dependency issue in brand-railway-icon.tsx that causes performance problems. The dependency array references non-memoized functions, defeating the purpose of useImperativeHandle optimization. All other implementations are clean and follow existing patterns.
  • icons/brand-railway-icon.tsx needs the dependency array fix before merge. icons/brand-vercel-icon.tsx has a minor style inconsistency that should be addressed.

Important Files Changed

File Analysis

Filename Score Overview
icons/brand-railway-icon.tsx 3/5 Renamed from railway-icon.tsx, has incorrect useImperativeHandle dependency array that causes unnecessary re-renders
icons/brand-vercel-icon.tsx 4/5 Renamed from vercel-icon.tsx, good implementation with useCallback, missing dependency array in useImperativeHandle
icons/brand-openai-icon.tsx 5/5 New animated OpenAI brand icon with path animation, clean implementation
icons/brand-react-icon.tsx 5/5 New animated React brand icon with complex orbit animations, well-structured
icons/brand-windows-icon.tsx 5/5 New animated Windows brand icon with elaborate multi-stage animation (explode, flip, snap back)
icons/index.ts 5/5 Updated imports and exports for all brand icons, properly renamed from old names to new brand-* naming convention
app/layout.tsx 5/5 Removed StickyBanner promotional component for evilcharts.com
package.json 5/5 Updated package name from animated-icons to itshover and description

Sequence Diagram

sequenceDiagram
    participant User
    participant Component
    participant Icon as Brand Icon Component
    participant Motion as motion/react
    participant Registry
    
    User->>Component: Import brand icon
    Component->>Icon: Render with props (size, color, etc)
    Icon->>Motion: Initialize useAnimate() hook
    Motion-->>Icon: Return [scope, animate]
    Icon->>Icon: Setup start() and stop() animations
    Icon->>Icon: Create useImperativeHandle
    Icon->>Motion: Render motion.svg with animations
    
    User->>Icon: Hover over icon
    Icon->>Icon: Call start()
    Icon->>Motion: Execute animation sequence
    Motion->>Icon: Animate paths/elements
    
    User->>Icon: Hover end
    Icon->>Icon: Call stop()
    Icon->>Motion: Reset animation state
    Motion->>Icon: Return to initial state
    
    User->>Registry: Install icon via CLI
    Registry->>User: Copy icon files and dependencies
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 11, 2026

Additional Comments (3)

icons/brand-railway-icon.tsx
useCallback is imported but never used - can be removed from imports

Prompt To Fix With AI
This is a comment left during a code review.
Path: icons/brand-railway-icon.tsx
Line: 1:1

Comment:
`useCallback` is imported but never used - can be removed from imports

How can I resolve this? If you propose a fix, please make it concise.

icons/brand-railway-icon.tsx
dependency array [start, stop] is incorrect - these functions are recreated on every render and will cause the useImperativeHandle to run on every render, defeating its memoization purpose. Either wrap start and stop with useCallback, or omit the dependency array since animate is stable from useAnimate

    useImperativeHandle(
      ref,
      () => ({
        startAnimation: start,
        stopAnimation: stop,
      }),
    );
Prompt To Fix With AI
This is a comment left during a code review.
Path: icons/brand-railway-icon.tsx
Line: 30:36

Comment:
dependency array `[start, stop]` is incorrect - these functions are recreated on every render and will cause the `useImperativeHandle` to run on every render, defeating its memoization purpose. Either wrap `start` and `stop` with `useCallback`, or omit the dependency array since `animate` is stable from `useAnimate`

```suggestion
    useImperativeHandle(
      ref,
      () => ({
        startAnimation: start,
        stopAnimation: stop,
      }),
    );
```

How can I resolve this? If you propose a fix, please make it concise.

icons/brand-vercel-icon.tsx
missing dependency array - since start and stop are memoized with useCallback, should include them in the dependency array for completeness

    useImperativeHandle(ref, () => ({
      startAnimation: start,
      stopAnimation: stop,
    }), [start, stop]);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: icons/brand-vercel-icon.tsx
Line: 28:31

Comment:
missing dependency array - since `start` and `stop` are memoized with `useCallback`, should include them in the dependency array for completeness

```suggestion
    useImperativeHandle(ref, () => ({
      startAnimation: start,
      stopAnimation: stop,
    }), [start, stop]);
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@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: 8

🤖 Fix all issues with AI agents
In @icons/brand-stripe-icon.tsx:
- Around line 1-3: The clipPath uses a static id ("fillClip") in BrandStripeIcon
which causes collisions when multiple icons render; update the component to
generate a unique id per instance (e.g., React's useId() or a short
random/monotonic suffix) and replace the static id and all
clipPath="url(#fillClip)" references with the unique id (e.g.,
id={`fillClip-${uid}`} and clipPath={`url(#fillClip-${uid})`}) so each
BrandStripeIcon instance references its own clipPath; update every occurrence in
the component (the id attribute definition and all clipPath/url(...) usages).

In @icons/brand-threads-icon.tsx:
- Around line 12-40: The animation is currently using the broad selector "path"
in the start and stop functions (inside start, the two animate calls and the
await animate; inside stop the two animate calls) which also targets the
invisible stroke="none" path; add a specific class (e.g. "animated-path") to the
visible SVG path element and update all animate selectors from "path" to
".animated-path" (and keep scope.current for container-level animations) so only
the intended path is animated and the invisible path is excluded.

In @icons/brand-zoom-icon.tsx:
- Around line 38-61: The animation selectors passed to animate(...) are invalid
("motion\\:nth-of-type(1)" / "motion\\:nth-of-type(2)") because motion.path
renders as a normal <path>; update the animate targets used in these
animate(...) calls to match actual DOM elements—either change them to
path:nth-of-type(1) and path:nth-of-type(2) (or more specific SVG selectors) or,
preferably, add className attributes to the motion.path elements (e.g.,
"zoom-camera", "zoom-body") and target those classes (".zoom-camera",
".zoom-body") in the animate(...) calls so the animations actually select and
run on the intended elements.

In @public/r/brand-grok-icon.json:
- Around line 9-12: The imperative API (startAnimation/stopAnimation via
useImperativeHandle) can run when scope.current is null; update the start and
stop callbacks (the start and stop functions used in useImperativeHandle) to
early-return if scope.current is falsy (e.g., if (!scope.current) return;)
before calling animate(scope.current, ...), or alternatively switch to
selector-based animate calls (e.g., animate('.grok-upper', ...) and
animate('.grok-lower', ...)) so null scope is avoided.

In @public/r/brand-stripe-icon.json:
- Around line 8-11: The clipPath uses a hardcoded id ("fillClip") causing
collisions when multiple BrandStripeIcon instances render; update
BrandStripeIcon to import and call React's useId(), assign the generated id to
the <clipPath> id and reference it from the motion.path's clipPath attribute via
`url(#${id})`, and ensure useId is used instead of the static "fillClip" on both
the <clipPath> element and the motion.path that uses it (also add the useId
import).

In @public/r/brand-zoom-icon.json:
- Line 10: The animation selectors "motion\\:nth-of-type(1)" and
"motion\\:nth-of-type(2)" won't match because motion.path renders as native
<path>; update BrandZoomIcon's start (and any animate calls) to target
class-based selectors instead: add unique className props to the two motion.path
elements (e.g., "brand-zoom-part-1" and "brand-zoom-part-2") and replace the
animate targets in start from the failing strings to ".brand-zoom-part-1" and
".brand-zoom-part-2" so the animate(...) calls actually select and animate those
paths; keep the rest of the animation logic in start/stop unchanged.
🧹 Nitpick comments (24)
icons/brand-threads-icon.tsx (1)

49-55: Consider removing unnecessary wrapper functions.

The handleHoverStart and handleHoverEnd functions simply forward calls to start() and stop(). You can pass these directly to the motion.svg props.

♻️ Optional simplification
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
       <motion.svg
         ref={scope}
-        onHoverStart={handleHoverStart}
-        onHoverEnd={handleHoverEnd}
+        onHoverStart={start}
+        onHoverEnd={stop}
icons/brand-react-native-icon.tsx (1)

63-73: Stop function doesn't need to be async.

The stop() function is declared async but only awaits one animation. The subsequent animations (lines 70, 72) run after the await completes, which may not be intentional. If you want all animations to reset simultaneously, remove the async/await.

This will be resolved by the fix suggested in the previous comment, which makes stop() synchronous.

icons/brand-cursor-icon.tsx (1)

45-45: Inconsistent className with component rename.

The component was renamed to BrandCursorIcon, but the className still uses cursor-ide-icon. Other brand icons use their brand name in the className (e.g., qwen-icon, notion-icon, gemini-icon). Consider updating for consistency.

Suggested fix
-        className={`cursor-ide-icon cursor-pointer ${className}`}
+        className={`cursor-icon cursor-pointer ${className}`}
icons/brand-paypal-icon.tsx (4)

1-1: Missing useCallback import.

This component should use useCallback for the start and stop functions to match the pattern used in other brand icons and prevent unnecessary re-renders.

Suggested fix
-import { forwardRef, useImperativeHandle } from "react";
+import { forwardRef, useImperativeHandle, useCallback } from "react";

12-26: Wrap animation functions in useCallback.

The start and stop functions are recreated on every render. Other brand icons (e.g., BrandQwenIcon, BrandNotionIcon) wrap these in useCallback for consistency and to avoid unnecessary re-renders.

Suggested fix
-    const start = async () => {
+    const start = useCallback(async () => {
       animate(".p-back", { x: 2, y: -2 }, { duration: 0.3, ease: "easeOut" });
       await animate(
         ".p-front",
         { x: -2, y: 2 },
         { duration: 0.3, ease: "easeOut" },
       );
       animate(".p-back", { x: 0, y: 0 }, { duration: 0.3, ease: "easeIn" });
       animate(".p-front", { x: 0, y: 0 }, { duration: 0.3, ease: "easeIn" });
-    };
+    }, [animate]);

-    const stop = () => {
+    const stop = useCallback(() => {
       animate(".p-back", { x: 0, y: 0 }, { duration: 0.2 });
       animate(".p-front", { x: 0, y: 0 }, { duration: 0.2 });
-    };
+    }, [animate]);

28-33: Add dependency array to useImperativeHandle.

The useImperativeHandle hook is missing its dependency array, causing the handle object to be recreated on every render. This is inconsistent with other brand icons.

Suggested fix
-    useImperativeHandle(ref, () => {
-      return {
-        startAnimation: start,
-        stopAnimation: stop,
-      };
-    });
+    useImperativeHandle(
+      ref,
+      () => ({
+        startAnimation: start,
+        stopAnimation: stop,
+      }),
+      [start, stop],
+    );

35-41: Remove redundant wrapper functions.

handleHoverStart and handleHoverEnd simply call start and stop. Pass them directly to reduce indirection, matching the pattern in other brand icons.

Suggested fix
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
       <motion.svg
         ref={scope}
-        onHoverStart={handleHoverStart}
-        onHoverEnd={handleHoverEnd}
+        onHoverStart={start}
+        onHoverEnd={stop}
icons/brand-telegram-icon.tsx (4)

1-1: Missing useCallback import.

Same pattern issue as brand-paypal-icon.tsx. Add useCallback to wrap the animation functions for consistency with other brand icons.

Suggested fix
-import { forwardRef, useImperativeHandle } from "react";
+import { forwardRef, useImperativeHandle, useCallback } from "react";

12-22: Wrap animation functions in useCallback.

The start and stop functions should be wrapped in useCallback to prevent recreation on every render.

Suggested fix
-    const start = async () => {
+    const start = useCallback(async () => {
       animate(
         ".plane",
         { x: [0, 10, -10, 0], y: [0, -10, 10, 0], opacity: [1, 0, 0, 1] },
         { duration: 1, times: [0, 0.4, 0.5, 1], ease: "easeInOut" },
       );
-    };
+    }, [animate]);

-    const stop = () => {
+    const stop = useCallback(() => {
       animate(".plane", { x: 0, y: 0, opacity: 1 });
-    };
+    }, [animate]);

24-29: Add dependency array to useImperativeHandle.

Missing dependency array causes unnecessary re-creation of the handle object.

Suggested fix
-    useImperativeHandle(ref, () => {
-      return {
-        startAnimation: start,
-        stopAnimation: stop,
-      };
-    });
+    useImperativeHandle(
+      ref,
+      () => ({
+        startAnimation: start,
+        stopAnimation: stop,
+      }),
+      [start, stop],
+    );

31-37: Remove redundant wrapper functions.

Pass start and stop directly to hover handlers.

Suggested fix
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
       <motion.svg
         ref={scope}
-        onHoverStart={handleHoverStart}
-        onHoverEnd={handleHoverEnd}
+        onHoverStart={start}
+        onHoverEnd={stop}
public/r/brand-aws-icon.json (1)

1-20: Empty stop function creates inconsistent hover behavior.

The embedded BrandAwsIcon component has an empty stop function (const stop = () => {};), which means onHoverEnd and stopAnimation do nothing. This differs from other brand icons (e.g., BrandOpenaiIcon, BrandVercelIcon) that reset animations on stop.

Consider adding reset logic to maintain consistent behavior across all brand icons.

icons/brand-aws-icon.tsx (2)

39-39: Empty stop function - animation cannot be interrupted.

The stop function is a no-op, meaning rapid hover interactions can stack multiple animation sequences. Consider resetting the animated elements to their initial state, similar to BrandOpenaiIcon:

Proposed fix
-    const stop = () => {};
+    const stop = () => {
+      animate(".smile", { pathOffset: 0, opacity: 1, pathLength: 1 }, { duration: 0.2 });
+      animate(".arrowhead", { scale: 1, opacity: 1 }, { duration: 0.2 });
+    };

12-37: Consider wrapping start with useCallback for consistency.

BrandVercelIcon wraps its animation functions with useCallback, but BrandAwsIcon does not. For consistency across brand icons and to prevent unnecessary re-creation on re-renders:

Proposed fix
+import { forwardRef, useImperativeHandle, useCallback } from "react";
-import { forwardRef, useImperativeHandle } from "react";
...
-    const start = async () => {
+    const start = useCallback(async () => {
       // animation logic...
-    };
+    }, [animate]);
icons/brand-vercel-icon.tsx (1)

47-47: Minor: Missing select-none class.

Other brand icons include select-none in their className to prevent text selection during interaction. Consider adding it for consistency:

-        className={`cursor-pointer ${className}`}
+        className={`cursor-pointer select-none ${className}`}
icons/brand-openai-icon.tsx (1)

12-30: Optional: Wrap animation functions with useCallback for consistency.

For consistency with BrandVercelIcon, consider wrapping start and stop with useCallback. This is a minor optimization since the functions are recreated on each render.

icons/brand-windows-icon.tsx (2)

131-136: Missing dependency array in useImperativeHandle.

Unlike other icons in this PR (e.g., BrandQwenIcon, BrandGeminiIcon), this component omits the dependency array for useImperativeHandle. This causes the handle object to be recreated on every render. For consistency and to prevent unnecessary re-creations, add a dependency array.

Proposed fix
-    useImperativeHandle(ref, () => {
-      return {
-        startAnimation: start,
-        stopAnimation: stop,
-      };
-    });
+    useImperativeHandle(ref, () => ({
+      startAnimation: start,
+      stopAnimation: stop,
+    }), [start, stop]);

Note: You may also want to wrap start and stop in useCallback to ensure stable references, similar to the pattern used in BrandQwenIcon and BrandGeminiIcon.


138-144: Redundant hover handler wrappers.

handleHoverStart and handleHoverEnd simply delegate to start and stop. You can pass start and stop directly to onHoverStart and onHoverEnd.

Proposed simplification
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
       <motion.svg
         ref={scope}
-        onHoverStart={handleHoverStart}
-        onHoverEnd={handleHoverEnd}
+        onHoverStart={start}
+        onHoverEnd={stop}
         ...
       >
icons/brand-zoom-icon.tsx (1)

93-98: Missing dependency array in useImperativeHandle.

For consistency with other icons in this PR, add a dependency array to prevent unnecessary handle recreation on every render.

Proposed fix
-    useImperativeHandle(ref, () => {
-      return {
-        startAnimation: start,
-        stopAnimation: stop,
-      };
-    });
+    useImperativeHandle(ref, () => ({
+      startAnimation: start,
+      stopAnimation: stop,
+    }), [start, stop]);
public/r/brand-railway-icon.json (1)

8-11: useCallback is imported but not used; start/stop should be memoized.

The component imports useCallback but doesn't use it. The start and stop async functions are recreated on every render, which defeats the purpose of the [start, stop] dependency array in useImperativeHandle.

Proposed fix

Either remove the unused useCallback import if memoization is not intended:

-import { forwardRef, useImperativeHandle, useCallback } from "react";
+import { forwardRef, useImperativeHandle } from "react";

Or wrap the functions with useCallback for proper memoization (recommended):

-    const start = async () => {
+    const start = useCallback(async () => {
       await animate(
         ".railway-inner",
         { opacity: 0 },
         { duration: 0.6, ease: "linear" },
       );
       await animate(
         ".railway-lower",
         { x: [0, -3, 3, 0] },
         { duration: 0.6, ease: "easeInOut" },
       );
-    };
+    }, [animate]);

-    const stop = async () => {
+    const stop = useCallback(async () => {
       await animate(
         ".railway-inner",
         { opacity: 1 },
         { duration: 0.2, ease: "linear" },
       );
-    };
+    }, [animate]);
icons/brand-react-icon.tsx (1)

77-82: Stabilize imperative handle to avoid churn across renders.
Line 77-Line 82 currently recreates the handle on each render; after memoizing start/stop (see above), pass them as deps to useImperativeHandle.

icons/brand-railway-icon.tsx (1)

1-3: Wrap start/stop in useCallback (fixes hook dependency churn + matches static analysis).
Line 30-Line 37 includes [start, stop], but Line 9 and Line 22 redefine those functions every render, so the deps always change.

Proposed fix
 import { forwardRef, useImperativeHandle, useCallback } from "react";
 import type { AnimatedIconHandle, AnimatedIconProps } from "./types";
 import { motion, useAnimate } from "motion/react";

 const BrandRailwayIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
   ({ size = 24, color = "currentColor", className = "" }, ref) => {
     const [scope, animate] = useAnimate();

-    const start = async () => {
+    const start = useCallback(async () => {
       await animate(
         ".railway-inner",
         { opacity: 0 },
         { duration: 0.6, ease: "linear" },
       );
       await animate(
         ".railway-lower",
         { x: [0, -3, 3, 0] },
         { duration: 0.6, ease: "easeInOut" },
       );
-    };
+    }, [animate]);

-    const stop = async () => {
+    const stop = useCallback(async () => {
       await animate(
         ".railway-inner",
         { opacity: 1 },
         { duration: 0.2, ease: "linear" },
       );
-    };
+    }, [animate]);

Also applies to: 5-37

public/r/brand-react-native-icon.json (1)

14-17: Consider AnimatedIconHandle return types as void | Promise<void> to match async icons.
Line 15 defines startAnimation: () => void / stopAnimation: () => void, but many icons in this PR use async start/stop (e.g., embedded at Line 10). Aligning the type makes it clearer for consumers (and allows await ref.current?.startAnimation() patterns).

public/r/brand-windows-icon.json (1)

1-20: Inconsistent hook patterns compared to BrandCursorIcon.

The embedded BrandWindowsIcon component doesn't wrap start and stop functions in useCallback and lacks a dependency array in useImperativeHandle. In contrast, BrandCursorIcon (in public/r/brand-cursor-icon.json) properly uses useCallback with dependencies and includes [start, stop] in useImperativeHandle.

For consistency and to avoid potential stale closure issues, consider aligning both implementations:

// In BrandWindowsIcon, wrap functions and add deps:
const start = useCallback(async () => { /* ... */ }, [animate, scope]);
const stop = useCallback(() => { /* ... */ }, [animate, scope]);

useImperativeHandle(ref, () => ({
  startAnimation: start,
  stopAnimation: stop,
}), [start, stop]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1239d and 8ee3b94.

📒 Files selected for processing (39)
  • app/layout.tsx
  • icons/brand-aws-icon.tsx
  • icons/brand-cursor-icon.tsx
  • icons/brand-gemini-icon.tsx
  • icons/brand-grok-icon.tsx
  • icons/brand-notion-icon.tsx
  • icons/brand-openai-icon.tsx
  • icons/brand-paypal-icon.tsx
  • icons/brand-qwen-icon.tsx
  • icons/brand-railway-icon.tsx
  • icons/brand-react-icon.tsx
  • icons/brand-react-native-icon.tsx
  • icons/brand-stripe-icon.tsx
  • icons/brand-telegram-icon.tsx
  • icons/brand-threads-icon.tsx
  • icons/brand-vercel-icon.tsx
  • icons/brand-windows-icon.tsx
  • icons/brand-zoom-icon.tsx
  • icons/index.ts
  • package.json
  • public/r/brand-aws-icon.json
  • public/r/brand-cursor-icon.json
  • public/r/brand-gemini-icon.json
  • public/r/brand-grok-icon.json
  • public/r/brand-notion-icon.json
  • public/r/brand-openai-icon.json
  • public/r/brand-paypal-icon.json
  • public/r/brand-qwen-icon.json
  • public/r/brand-railway-icon.json
  • public/r/brand-react-icon.json
  • public/r/brand-react-native-icon.json
  • public/r/brand-stripe-icon.json
  • public/r/brand-telegram-icon.json
  • public/r/brand-threads-icon.json
  • public/r/brand-vercel-icon.json
  • public/r/brand-windows-icon.json
  • public/r/brand-zoom-icon.json
  • public/r/registry.json
  • registry.json
💤 Files with no reviewable changes (1)
  • app/layout.tsx
🧰 Additional context used
🪛 GitHub Check: build
icons/brand-railway-icon.tsx

[warning] 22-22:
The 'stop' function makes the dependencies of useImperativeHandle Hook (at line 36) change on every render. To fix this, wrap the definition of 'stop' in its own useCallback() Hook


[warning] 9-9:
The 'start' function makes the dependencies of useImperativeHandle Hook (at line 36) change on every render. To fix this, wrap the definition of 'start' in its own useCallback() Hook

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🔇 Additional comments (22)
package.json (1)

2-5: LGTM! Clean rebranding update.

The package name and description changes align well with the broader PR objective of introducing brand-focused animated icons.

icons/brand-grok-icon.tsx (1)

5-5: LGTM! Clean refactoring for brand consistency.

The renaming to BrandGrokIcon aligns with the PR's brand icon standardization. The opacity change from 0.5 to 1.0 in the resting state improves visibility, and the className addition maintains styling consistency.

Also applies to: 37-37, 43-43, 62-62, 83-84

public/r/brand-threads-icon.json (1)

1-20: Regenerate after fixing source file issues.

This registry file is auto-generated (via the registry:build script). After addressing the animation selector issue in icons/brand-threads-icon.tsx, regenerate this file to ensure consistency.

Run the following command to regenerate the registry files after making source changes:

npm run registry:build
icons/brand-qwen-icon.tsx (1)

5-62: LGTM!

The rename to BrandQwenIcon is consistent with the registry-wide branding updates. The animation logic with useCallback and proper dependency arrays is correctly implemented, and the imperative handle exposure follows the established pattern.

icons/brand-notion-icon.tsx (1)

5-71: LGTM!

The rename to BrandNotionIcon aligns with the branding convention. The animation approach using class selectors (.notion-shell, .notion-letter) for targeting child elements is appropriate, and the useCallback dependencies correctly include only animate.

icons/brand-gemini-icon.tsx (1)

5-73: LGTM!

The rename to BrandGeminiIcon is consistent with the branding convention. The implementation correctly uses useCallback with proper dependencies, and includes good accessibility attributes (role="img", aria-label, title). The transformBox and transformOrigin styles ensure the rotation animation centers correctly.

public/r/brand-paypal-icon.json (1)

7-17: Ensure registry file stays in sync with source.

The embedded code in this registry file should be regenerated after applying the recommended refactors to icons/brand-paypal-icon.tsx (adding useCallback, dependency array for useImperativeHandle, removing wrapper functions).

public/r/brand-openai-icon.json (1)

1-20: LGTM!

The registry structure is correct. The embedded BrandOpenaiIcon component properly implements both start and stop animations with staggered delays for a polished visual effect.

icons/brand-vercel-icon.tsx (1)

20-26: Verify intended rotation behavior on stop.

The stop function rotates to rotateY: -180 rather than resetting to 0. After a hover cycle (start→180°, stop→-180°), the element has effectively rotated 360°. If the intent is to return to the initial state, consider:

Potential fix if reset is intended
     const stop = useCallback(async () => {
       await animate(
         ".triangle",
-        { rotateY: -180 },
+        { rotateY: 0 },
         { duration: 0.6, ease: "easeInOut" },
       );
     }, [animate]);
icons/brand-openai-icon.tsx (1)

1-79: LGTM!

The component properly implements start/stop animations with staggered delays. The stop function correctly resets the animation state, unlike some other brand icons.

public/r/brand-vercel-icon.json (1)

1-20: LGTM for registry structure.

The registry entry is correctly configured. Note: The same rotation behavior concern flagged in icons/brand-vercel-icon.tsx applies to the embedded content here.

public/r/brand-qwen-icon.json (1)

1-20: LGTM!

The component follows proper React patterns with useCallback for memoizing animation functions and correct dependency arrays for useImperativeHandle. The wobble animation logic is well-implemented.

public/r/brand-telegram-icon.json (1)

1-20: Implementation is functional with minor consistency notes.

The class-based selector (.plane) is a good approach for targeting animated elements. Consider adding a dependency array to useImperativeHandle for consistency with icons like BrandQwenIcon and BrandGeminiIcon.

public/r/brand-gemini-icon.json (1)

1-20: LGTM!

Excellent implementation with proper useCallback memoization, correct dependency arrays, and good accessibility attributes (role="img", aria-label, <title>). This can serve as a reference pattern for other icons in this PR.

public/r/brand-react-icon.json (1)

8-12: Keep registry-embedded BrandReactIcon behavior in sync with icons/brand-react-icon.tsx fixes.
Once you address the animation cancellation / repeat behavior in icons/brand-react-icon.tsx, ensure the embedded content here is updated accordingly (to prevent the registry version drifting).

public/r/brand-notion-icon.json (1)

9-12: Verify rotateY on SVG elements behaves as intended across browsers.
The embedded start() animates rotateY on .notion-shell; depending on motion/react + browser SVG transform support, this may not render (or may render differently). If it’s flaky, prefer purely 2D transforms (e.g., rotate/skewX/skewY) or wrap in a <g> with an appropriate transform style.

public/r/brand-cursor-icon.json (1)

1-20: LGTM!

The BrandCursorIcon implementation properly uses useCallback with appropriate dependencies and includes a dependency array in useImperativeHandle. This follows React best practices for stable function references and proper ref handle updates.

icons/index.ts (3)

42-51: LGTM!

The new brand icon imports are properly organized and follow the existing naming convention.


1396-1445: LGTM!

The new brand icon entries follow the established pattern with appropriate keywords for discoverability. The sorting at line 1489 ensures consistent alphabetical ordering.


1464-1488: LGTM!

The renamed icon entries (brand-railway-icon, brand-cursor-icon, brand-notion-icon, brand-gemini-icon, brand-qwen-icon, brand-grok-icon) are properly updated with new names and component references while preserving their keywords.

public/r/registry.json (1)

687-1009: LGTM!

The new brand icon registry entries are correctly structured and consistent with existing entries. Since this is an auto-generated file, the additions appear to be properly generated from the source registry.

registry.json (1)

687-1009: LGTM!

The brand icon entries are correctly structured. Both registry.json files appear to be auto-generated outputs that should remain in sync.

Comment on lines +12 to +49
const start = async () => {
await animate(
".center-dot",
{ scale: [1, 1.4, 1.2] },
{ duration: 0.4, ease: [0.34, 1.56, 0.64, 1] },
);

animate(".orbit-1", { rotate: 360 }, { duration: 1, ease: "linear" });
animate(".orbit-2", { rotate: -360 }, { duration: 2, ease: "linear" });
animate(".orbit-3", { rotate: 360 }, { duration: 4, ease: "linear" });

// Pulsing center dot
animate(
".center-dot",
{
scale: [1.2, 1.35, 1.2],
opacity: [1, 0.8, 1],
},
{ duration: 2, ease: "easeInOut", repeat: Infinity },
);

// Subtle breathing effect on all orbits
animate(
".orbit-1",
{ scale: [1, 1.02, 1] },
{ duration: 3, ease: "easeInOut", repeat: Infinity },
);
animate(
".orbit-2",
{ scale: [1, 1.03, 1] },
{ duration: 3.5, ease: "easeInOut", repeat: Infinity, delay: 0.5 },
);
animate(
".orbit-3",
{ scale: [1, 1.02, 1] },
{ duration: 4, ease: "easeInOut", repeat: Infinity, delay: 1 },
);
};
Copy link
Copy Markdown
Contributor

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:

find . -name "brand-react-icon.tsx" -type f

Repository: itshover/itshover

Length of output: 89


🏁 Script executed:

cat -n ./icons/brand-react-icon.tsx

Repository: itshover/itshover

Length of output: 5298


🌐 Web query:

motion/react useAnimate API animation cancellation stop

💡 Result:

Short answer: call the animation controls returned by animate — e.g. controls.stop() to stop (and apply final styles) or controls.cancel() to abort without applying final styles (cancel is the WAAPI behavior). Use these in event handlers or cleanup (return () => controls.stop()/controls.cancel()). [1][2][3]

Example:

  • Using useAnimate (from motion/react)
    const [scope, animate] = useAnimate();
    useEffect(() => {
    const controls = animate(scope.current, { x: "100%" }, { duration: 1 });
    return () => controls.stop(); // or controls.cancel()
    }, []);

Sources:

  • motion.dev useAnimate docs. [1]
  • animate() / AnimationControls methods (play/pause/stop/cancel) reference. [2]
  • Web Animations API: Animation.cancel() behavior. [3]

🌐 Web query:

motion/react animate same selector multiple times overlapping animations stacking behavior

💡 Result:

Short answer

  • When you trigger multiple animations on the same element/property, the most recent animation that modifies a given CSS property typically wins (overrides) — properties are not automatically merged or queued. Use explicit sequencing or different properties to avoid conflicts. [1][2]

How to handle/avoid overlapping stacking issues

  • Sequence animations with imperative controls (useAnimate or useAnimation/useAnimationControls) and await each animation so they don’t overlap. useAnimate returns an animate function that supports async/await/timelines. Example: await animate(selector, {...}); await animate(...). [1][3]
  • Use variants (or AnimationControls.start) to coordinate states instead of firing independent animate props. Variants let children follow a single coordinated state. [2]
  • Prefer different properties (or composite transforms) rather than animating the same property concurrently — last-writer wins. [2]
  • For transient interactions (hover/tap) note priority rules: while-* props have defined priorities and will fall back to animate/style values when removed; understand the priority order so values don’t unexpectedly snap. [4]
  • If you need a “restart” rather than overlap, change the key (force remount) or explicitly stop/reset the previous animation before starting a new one (controls.stop()). [3]

References
[1] Motion docs — useAnimate (React) (manual controls, scoped selectors, async/timeline examples).
[2] Motion upgrade/behavior notes — animation priority and fallback (“priority = ['animate','while-','exit']”) and how values are resolved.
[3] StackOverflow: sequencing animations with useAnimate / waiting for animations to finish (example using async/await).
[4] StackOverflow / docs: whileHover vs animate behavior and priority.


Store animation controls and cancel infinite animations on restart to prevent accumulation during rapid hover.

Lines 24–48 schedule multiple repeat: Infinity animations without storing their controls, and the stop() function (lines 51–75) only animates elements to a rest state—it does not cancel the underlying infinite animations. Rapid hover in/out triggers multiple start() calls, causing animations to stack and continue running in the background.

Store the returned animation controls in a useRef and call .stop() on all previous controls when start() is called:

Example pattern
-import { forwardRef, useImperativeHandle } from "react";
+import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
 import { motion, useAnimate } from "motion/react";

 const BrandReactIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
   (
     { size = 24, color = "currentColor", strokeWidth = 2, className = "" },
     ref,
   ) => {
     const [scope, animate] = useAnimate();
+    const running = useRef<any[]>([]);

-    const start = async () => {
+    const start = useCallback(async () => {
+      running.current.forEach((c) => c?.stop?.());
+      running.current = [];
+
       await animate(
         ".center-dot",
         { scale: [1, 1.4, 1.2] },
         { duration: 0.4, ease: [0.34, 1.56, 0.64, 1] },
       );

-      animate(".orbit-1", { rotate: 360 }, { duration: 1, ease: "linear" });
+      running.current.push(
+        animate(".orbit-1", { rotate: 360 }, { duration: 1, ease: "linear", repeat: Infinity }),
+      );
       // ... repeat for remaining infinite animations
-    };
+    }, [animate]);

Comment thread icons/brand-react-native-icon.tsx
Comment thread icons/brand-stripe-icon.tsx Outdated
Comment thread icons/brand-threads-icon.tsx
Comment thread icons/brand-zoom-icon.tsx
Comment on lines +38 to +61
animate(
"motion\\:nth-of-type(1)",
{
x: [0, 3, 0],
opacity: [1, 0.7, 1],
},
{
duration: 0.3,
repeat: 2,
ease: "easeInOut",
},
);

await animate(
"motion\\:nth-of-type(2)",
{
scale: [1, 1.08, 1.05, 1],
fill: ["none", "rgba(59, 130, 246, 0.1)", "none"],
},
{
duration: 0.5,
ease: "easeOut",
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Invalid CSS selector will cause animations to silently fail.

The selectors "motion\\:nth-of-type(1)" and "motion\\:nth-of-type(2)" are invalid. motion.path from motion/react renders as a standard <path> element in the DOM, not <motion:path>. These selectors won't match any elements, so the micro-animations will silently do nothing.

Proposed fix using path selectors
       animate(
-        "motion\\:nth-of-type(1)",
+        "path:nth-of-type(2)",
         {
           x: [0, 3, 0],
           opacity: [1, 0.7, 1],
         },
         ...
       );

       await animate(
-        "motion\\:nth-of-type(2)",
+        "path:nth-of-type(3)",
         {
           scale: [1, 1.08, 1.05, 1],
           fill: ["none", "rgba(59, 130, 246, 0.1)", "none"],
         },
         ...
       );

Alternatively, add class names to the motion.path elements and target them by class (e.g., .zoom-camera, .zoom-body), which is more robust than positional selectors.

🤖 Prompt for AI Agents
In @icons/brand-zoom-icon.tsx around lines 38 - 61, The animation selectors
passed to animate(...) are invalid ("motion\\:nth-of-type(1)" /
"motion\\:nth-of-type(2)") because motion.path renders as a normal <path>;
update the animate targets used in these animate(...) calls to match actual DOM
elements—either change them to path:nth-of-type(1) and path:nth-of-type(2) (or
more specific SVG selectors) or, preferably, add className attributes to the
motion.path elements (e.g., "zoom-camera", "zoom-body") and target those classes
(".zoom-camera", ".zoom-body") in the animate(...) calls so the animations
actually select and run on the intended elements.

Comment on lines +9 to +12
"path": "icons/brand-grok-icon.tsx",
"content": "import { forwardRef, useImperativeHandle, useCallback } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst BrandGrokIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n ({ size = 24, color = \"currentColor\", className = \"\" }, ref) => {\n const [scope, animate] = useAnimate();\n\n const start = useCallback(() => {\n // Subtle container pulse\n animate(\n scope.current,\n { scale: 1.05 },\n { duration: 0.25, ease: \"easeOut\" },\n );\n\n // Upper flows up-right\n animate(\n \".grok-upper\",\n { x: 3, y: -3, opacity: 0.85 },\n { duration: 0.35, ease: \"easeOut\" },\n );\n\n // Lower flows down-left\n animate(\n \".grok-lower\",\n { x: -3, y: 3, opacity: 0.85 },\n { duration: 0.35, ease: \"easeOut\" },\n );\n }, [animate, scope]);\n\n const stop = useCallback(() => {\n animate(scope.current, { scale: 1 }, { duration: 0.2 });\n\n animate(\n \".grok-upper\",\n { x: 0, y: 0, opacity: 1 },\n { duration: 0.25, ease: \"easeInOut\" },\n );\n\n animate(\n \".grok-lower\",\n { x: 0, y: 0, opacity: 1 },\n { duration: 0.25, ease: \"easeInOut\" },\n );\n }, [animate, scope]);\n\n useImperativeHandle(ref, () => ({\n startAnimation: start,\n stopAnimation: stop,\n }));\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={start}\n onHoverEnd={stop}\n fill={color}\n viewBox=\"0 0 24 24\"\n width={size}\n height={size}\n className={`grok-icon cursor-pointer ${className}`}\n style={{ flex: \"none\", lineHeight: 1 }}\n >\n <title>Grok</title>\n\n {/* Upper shard */}\n <motion.path\n className=\"grok-upper\"\n d=\"M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292\"\n />\n\n {/* Lower shard */}\n <motion.path\n className=\"grok-lower\"\n d=\"M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815\"\n />\n </motion.svg>\n );\n },\n);\n\nBrandGrokIcon.displayName = \"BrandGrokIcon\";\nexport default BrandGrokIcon;\n",
"type": "registry:ui"
},
Copy link
Copy Markdown
Contributor

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

🌐 Web query:

motion/react useAnimate scope.current imperative API safety

💡 Result:

Short summary — is it safe? Yes, useAnimate’s imperative API is designed for safe, scoped animations, but follow a few rules to avoid null refs, memory leaks, or race conditions.

Quick rules

  • Always attach scope to an element (ref on an HTML/SVG or motion component) and treat scope.current as possibly null — check it or run animate inside effects that run after mount. [1][2]
  • Start/stop from effects or event handlers; prefer async flows that await animations so you don’t race unmount. Example patterns shown in the docs use useEffect and await animate(...). [1][2]
  • rely on useAnimate’s automatic cleanup: animations started via the scoped animate are cleaned up when the component unmounts. Still explicitly stop long-running controls where appropriate (controls.stop()). [1][2]
  • For exit animations, combine with usePresence/useSafeToRemove and call safeToRemove after awaiting exit animations (docs example). This prevents premature unmounting. [1][2]
  • Don’t hold animate and call it after the component is unmounted. If you store async animation promises, guard with isMounted flags or check scope.current before continuing.
  • Avoid manipulating DOM outside the scope or conflicting with React-controlled layout/props; prefer animate for visual-only transitions.

Practical checklist (minimal)

  • ref={scope} on rendered element. [1]
  • call animate(scope.current, ...) or animate("selector", ...) inside useEffect/event handlers; check scope.current !== null. [1][2]
  • cancel/stop returned controls on cleanup when needed. [5] (controls.stop in docs). [1][5]
  • use usePresence for safe exit sequences. [1][2]

Sources

  • Motion docs: useAnimate (React) — usage, scoped selectors, automatic cleanup, examples. [1][2]

🏁 Script executed:

# Search for any existing patterns in the codebase with useAnimate or motion/react
rg "useAnimate" -A 5 -B 5

Repository: itshover/itshover

Length of output: 50374


🏁 Script executed:

# Check if there are other animated icon implementations that might show patterns
fd "icon.*tsx" --type f | head -20

Repository: itshover/itshover

Length of output: 705


Add null guard for scope.current in imperative API callbacks.

The startAnimation and stopAnimation functions exposed via useImperativeHandle can be called programmatically at any time, including during unmounting or edge cases where scope.current hasn't been initialized. Per motion/react documentation, scope.current should be treated as possibly null. Add a guard check at the start of both callbacks:

if (!scope.current) return;

Alternatively, use selector-based animations (e.g., animate(".grok-upper", ...)) like several other icons in the codebase, which handle null checks internally.

🤖 Prompt for AI Agents
In @public/r/brand-grok-icon.json around lines 9 - 12, The imperative API
(startAnimation/stopAnimation via useImperativeHandle) can run when
scope.current is null; update the start and stop callbacks (the start and stop
functions used in useImperativeHandle) to early-return if scope.current is falsy
(e.g., if (!scope.current) return;) before calling animate(scope.current, ...),
or alternatively switch to selector-based animate calls (e.g.,
animate('.grok-upper', ...) and animate('.grok-lower', ...)) so null scope is
avoided.

Comment thread public/r/brand-stripe-icon.json
"files": [
{
"path": "icons/brand-zoom-icon.tsx",
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst BrandZoomIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n await animate(\n scope.current,\n {\n scale: [1, 0.7],\n opacity: [1, 0.6],\n },\n {\n duration: 0.2,\n ease: \"easeIn\",\n },\n );\n await animate(\n scope.current,\n {\n scale: [0.7, 1.3, 1.15],\n opacity: [0.6, 1],\n rotateY: [0, 360],\n rotateX: [0, 15, 0],\n },\n {\n duration: 0.6,\n ease: [0.34, 1.56, 0.64, 1],\n },\n );\n\n animate(\n \"motion\\\\:nth-of-type(1)\",\n {\n x: [0, 3, 0],\n opacity: [1, 0.7, 1],\n },\n {\n duration: 0.3,\n repeat: 2,\n ease: \"easeInOut\",\n },\n );\n\n await animate(\n \"motion\\\\:nth-of-type(2)\",\n {\n scale: [1, 1.08, 1.05, 1],\n fill: [\"none\", \"rgba(59, 130, 246, 0.1)\", \"none\"],\n },\n {\n duration: 0.5,\n ease: \"easeOut\",\n },\n );\n\n animate(\n scope.current,\n {\n y: [0, -2, 0],\n scale: [1.15, 1.05, 1],\n },\n {\n duration: 0.4,\n ease: \"easeOut\",\n },\n );\n };\n\n const stop = () => {\n animate(\n scope.current,\n {\n scale: 1,\n rotateY: 0,\n rotateX: 0,\n y: 0,\n opacity: 1,\n },\n {\n duration: 0.3,\n ease: \"easeOut\",\n },\n );\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{\n perspective: \"1000px\",\n transformStyle: \"preserve-3d\",\n }}\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n <motion.path\n d=\"M17.011 9.385v5.128l3.989 3.487v-12l-3.989 3.385\"\n initial={{ x: 0, opacity: 1 }}\n />\n <motion.path\n d=\"M3.887 6h10.08c1.468 0 3.033 1.203 3.033 2.803v8.196a.991 .991 0 0 1 -.975 1h-10.373c-1.667 0 -2.652 -1.5 -2.652 -3l.01 -8a.882 .882 0 0 1 .208 -.71a.841 .841 0 0 1 .67 -.287l-.001 -.002\"\n initial={{ scale: 1, fill: \"none\" }}\n />\n </motion.svg>\n );\n },\n);\n\nBrandZoomIcon.displayName = \"BrandZoomIcon\";\n\nexport default BrandZoomIcon;\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Selector motion\\:nth-of-type likely won't match any elements.

The embedded component uses selectors like "motion\\:nth-of-type(1)" and "motion\\:nth-of-type(2)". However, motion.path renders as a native <path> element, not a <motion> element. These selectors will silently fail to match anything.

Consider using class-based selectors instead, similar to other brand icons:

Proposed fix
-        <motion.path
-          d="M17.011 9.385v5.128l3.989 3.487v-12l-3.989 3.385"
-          initial={{ x: 0, opacity: 1 }}
-        />
-        <motion.path
-          d="M3.887 6h10.08c1.468..."
-          initial={{ scale: 1, fill: "none" }}
-        />
+        <motion.path
+          className="camera-lens"
+          d="M17.011 9.385v5.128l3.989 3.487v-12l-3.989 3.385"
+          initial={{ x: 0, opacity: 1 }}
+        />
+        <motion.path
+          className="camera-body"
+          d="M3.887 6h10.08c1.468..."
+          initial={{ scale: 1, fill: "none" }}
+        />

Then update the animation selectors:

-        "motion\\:nth-of-type(1)",
+        ".camera-lens",
...
-        "motion\\:nth-of-type(2)",
+        ".camera-body",
📝 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
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst BrandZoomIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n await animate(\n scope.current,\n {\n scale: [1, 0.7],\n opacity: [1, 0.6],\n },\n {\n duration: 0.2,\n ease: \"easeIn\",\n },\n );\n await animate(\n scope.current,\n {\n scale: [0.7, 1.3, 1.15],\n opacity: [0.6, 1],\n rotateY: [0, 360],\n rotateX: [0, 15, 0],\n },\n {\n duration: 0.6,\n ease: [0.34, 1.56, 0.64, 1],\n },\n );\n\n animate(\n \"motion\\\\:nth-of-type(1)\",\n {\n x: [0, 3, 0],\n opacity: [1, 0.7, 1],\n },\n {\n duration: 0.3,\n repeat: 2,\n ease: \"easeInOut\",\n },\n );\n\n await animate(\n \"motion\\\\:nth-of-type(2)\",\n {\n scale: [1, 1.08, 1.05, 1],\n fill: [\"none\", \"rgba(59, 130, 246, 0.1)\", \"none\"],\n },\n {\n duration: 0.5,\n ease: \"easeOut\",\n },\n );\n\n animate(\n scope.current,\n {\n y: [0, -2, 0],\n scale: [1.15, 1.05, 1],\n },\n {\n duration: 0.4,\n ease: \"easeOut\",\n },\n );\n };\n\n const stop = () => {\n animate(\n scope.current,\n {\n scale: 1,\n rotateY: 0,\n rotateX: 0,\n y: 0,\n opacity: 1,\n },\n {\n duration: 0.3,\n ease: \"easeOut\",\n },\n );\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{\n perspective: \"1000px\",\n transformStyle: \"preserve-3d\",\n }}\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n <motion.path\n d=\"M17.011 9.385v5.128l3.989 3.487v-12l-3.989 3.385\"\n initial={{ x: 0, opacity: 1 }}\n />\n <motion.path\n d=\"M3.887 6h10.08c1.468 0 3.033 1.203 3.033 2.803v8.196a.991 .991 0 0 1 -.975 1h-10.373c-1.667 0 -2.652 -1.5 -2.652 -3l.01 -8a.882 .882 0 0 1 .208 -.71a.841 .841 0 0 1 .67 -.287l-.001 -.002\"\n initial={{ scale: 1, fill: \"none\" }}\n />\n </motion.svg>\n );\n },\n);\n\nBrandZoomIcon.displayName = \"BrandZoomIcon\";\n\nexport default BrandZoomIcon;\n",
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst BrandZoomIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n await animate(\n scope.current,\n {\n scale: [1, 0.7],\n opacity: [1, 0.6],\n },\n {\n duration: 0.2,\n ease: \"easeIn\",\n },\n );\n await animate(\n scope.current,\n {\n scale: [0.7, 1.3, 1.15],\n opacity: [0.6, 1],\n rotateY: [0, 360],\n rotateX: [0, 15, 0],\n },\n {\n duration: 0.6,\n ease: [0.34, 1.56, 0.64, 1],\n },\n );\n\n animate(\n \".camera-lens\",\n {\n x: [0, 3, 0],\n opacity: [1, 0.7, 1],\n },\n {\n duration: 0.3,\n repeat: 2,\n ease: \"easeInOut\",\n },\n );\n\n await animate(\n \".camera-body\",\n {\n scale: [1, 1.08, 1.05, 1],\n fill: [\"none\", \"rgba(59, 130, 246, 0.1)\", \"none\"],\n },\n {\n duration: 0.5,\n ease: \"easeOut\",\n },\n );\n\n animate(\n scope.current,\n {\n y: [0, -2, 0],\n scale: [1.15, 1.05, 1],\n },\n {\n duration: 0.4,\n ease: \"easeOut\",\n },\n );\n };\n\n const stop = () => {\n animate(\n scope.current,\n {\n scale: 1,\n rotateY: 0,\n rotateX: 0,\n y: 0,\n opacity: 1,\n },\n {\n duration: 0.3,\n ease: \"easeOut\",\n },\n );\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{\n perspective: \"1000px\",\n transformStyle: \"preserve-3d\",\n }}\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n <motion.path\n className=\"camera-lens\"\n d=\"M17.011 9.385v5.128l3.989 3.487v-12l-3.989 3.385\"\n initial={{ x: 0, opacity: 1 }}\n />\n <motion.path\n className=\"camera-body\"\n d=\"M3.887 6h10.08c1.468 0 3.033 1.203 3.033 2.803v8.196a.991 .991 0 0 1 -.975 1h-10.373c-1.667 0 -2.652 -1.5 -2.652 -3l.01 -8a.882 .882 0 0 1 .208 -.71a.841 .841 0 0 1 .67 -.287l-.001 -.002\"\n initial={{ scale: 1, fill: \"none\" }}\n />\n </motion.svg>\n );\n },\n);\n\nBrandZoomIcon.displayName = \"BrandZoomIcon\";\n\nexport default BrandZoomIcon;\n",
🤖 Prompt for AI Agents
In @public/r/brand-zoom-icon.json at line 10, The animation selectors
"motion\\:nth-of-type(1)" and "motion\\:nth-of-type(2)" won't match because
motion.path renders as native <path>; update BrandZoomIcon's start (and any
animate calls) to target class-based selectors instead: add unique className
props to the two motion.path elements (e.g., "brand-zoom-part-1" and
"brand-zoom-part-2") and replace the animate targets in start from the failing
strings to ".brand-zoom-part-1" and ".brand-zoom-part-2" so the animate(...)
calls actually select and animate those paths; keep the rest of the animation
logic in start/stop unchanged.

Copy link
Copy Markdown
Contributor

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
icons/accessibility-icon.tsx (1)

24-28: Critical: Person animation is not tracked.

The person animation is not added to animationControls.current, creating an inconsistency with the new architecture. This means:

  • If start() is called again while the person animation is running, it won't be stopped first
  • If stop() is called before the person animation completes, it may not be properly canceled
  • The animation handle is lost, potentially causing memory leaks or unintended behavior
🔧 Proposed fix
-      );
-      animate(
+      );
+      animationControls.current.push(
+        animate(
-        ".person",
-        { y: [0, -2, 0] },
-        { duration: 0.6, ease: "easeInOut" },
+          ".person",
+          { y: [0, -2, 0] },
+          { duration: 0.6, ease: "easeInOut" },
+        ),
       );
icons/alarm-clock-plus-icon.tsx (1)

13-50: Track the plus animation to prevent overlapping animations.

The plus animation (lines 45-49) is not stored in animationControls, while clock and bells animations are tracked. If start() is called rapidly (e.g., quick hover events), multiple plus animations could run simultaneously, potentially causing visual glitches.

For consistency and correctness, track all animations:

✨ Proposed fix to track the plus animation
-      await animate(
+      animationControls.current.push(
+        animate(
-        ".plus",
-        { scale: [1, 1.2, 1] },
-        { duration: 0.4, ease: "easeOut" },
+          ".plus",
+          { scale: [1, 1.2, 1] },
+          { duration: 0.4, ease: "easeOut" },
+        ),
-      );
+      );
+      
+      await animationControls.current[animationControls.current.length - 1];
icons/docker-icon.tsx (1)

47-50: Update AnimatedIconHandle type to match implementation.

The type definition declares startAnimation: () => void but the implementation is async () => {...}, which returns Promise<void>. Update icons/types.ts line 13 to:

startAnimation: () => Promise<void>;

This ensures downstream code using the ref handle won't have TypeScript errors.

icons/ambulance-icon.tsx (1)

11-29: Track the .plus animation too (rapid hover can stack multiple scale animations).

Right now only .ambulance is stored/stopped; the .plus animation can be re-triggered while a previous one is still active.

Proposed refactor (track both + centralize stop/clear + unmount cleanup)
-import { forwardRef, useImperativeHandle, useRef } from "react";
+import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
 import type { AnimatedIconHandle, AnimatedIconProps } from "./types";
 import { motion, useAnimate } from "motion/react";

@@ -10,7 +10,12 @@ const AmbulanceIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
     const [scope, animate] = useAnimate();
     const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);

+    const stopControls = () => {
+      animationControls.current.forEach((control) => control.stop());
+      animationControls.current = [];
+    };
+
+    useEffect(() => () => stopControls(), []);
+
     const start = async () => {
-      animationControls.current.forEach((control) => control.stop());
-      animationControls.current = [];
+      stopControls();

       animationControls.current.push(
         animate(
@@ -19,14 +24,12 @@ const AmbulanceIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
           { duration: 0.6, ease: "easeInOut", repeat: Infinity },
         ),
       );
-      animate(
-        ".plus",
-        { scale: [1, 1.3, 1] },
-        { duration: 0.6, ease: "easeInOut" },
-      );
+      animationControls.current.push(
+        animate(".plus", { scale: [1, 1.3, 1] }, { duration: 0.6, ease: "easeInOut" }),
+      );
     };

     const stop = () => {
-      animationControls.current.forEach((control) => control.stop());
-      animationControls.current = [];
+      stopControls();

       animate(".ambulance", { x: 0 }, { duration: 0.2, ease: "easeInOut" });
       animate(".plus", { scale: 1 }, { duration: 0.2, ease: "easeInOut" });

Also applies to: 31-37

🤖 Fix all issues with AI agents
In @icons/brand-railway-icon.tsx:
- Around line 9-33: The exported imperative handle methods have a type mismatch:
the async functions start and stop (Promise<void>) are being exposed as
startAnimation and stopAnimation typed as () => void; update the
AnimatedIconHandle interface in icons/types.ts so startAnimation and
stopAnimation are typed as () => Promise<void> (or change the handle to return
the existing Promise) and ensure useImperativeHandle returns the start and stop
functions as-is; alternatively, if you prefer synchronous methods, remove
async/await from the start and stop functions and keep the interface as () =>
void, but the recommended fix is to change AnimatedIconHandle to use
Promise<void> for both methods.

In @icons/brand-stripe-icon.tsx:
- Around line 14-31: The start function is declared async (start) but
AnimatedIconHandle defines startAnimation: () => void, causing a type mismatch
and asymmetry with stop; either make both functions synchronous by removing
async/await from start and calling animate without awaiting so start returns
void (and keep stop as-is), or change the AnimatedIconHandle signature to
startAnimation: () => Promise<void> and update all icon components (and any
callers) to return/handle Promise<void> for both start and stop to keep the API
consistent; ensure useImperativeHandle returns the matching types for
startAnimation and stopAnimation and update any related types/usages.

In @icons/brand-threads-icon.tsx:
- Around line 12-35: The start function can spawn overlapping animations on
rapid hover; before beginning new animations in start, cancel/stop any existing
animations created for scope.current and ".animated-path" (store animation
handles/controls returned by animate in a ref like currentAnimationRef), then
start the new animations and save their handles; also ensure you stop those
handles on unmount (cleanup in useEffect) so scope.current animations are always
cancelled before starting new ones.

In @public/r/accessibility-icon.json:
- Line 10: The start() function currently pushes only the wheel animation
control into animationControls.current but calls animate(".person", ...) without
storing its returned control, so overlapping person animations won't be stopped;
update start() to capture the return value of animate(".person", { y: [...] }, {
... }) and push that control into animationControls.current (alongside the wheel
control) so stop() can stop all running animations; reference start(), stop(),
animationControls.current and the animate(".person", ...) call when making this
change.

In @public/r/alarm-clock-plus-icon.json:
- Around line 9-10: The .plus animation isn't being tracked or stopped: in
start() push the animate(...) call for the ".plus" tween into
animationControls.current (like the pushes for ".clock" and ".bells") so it can
be stopped later, and in stop() iterate/stop those controls and also ensure you
reset the plus element to scale 1 (e.g., call animate(".plus", { scale: 1 }, {
duration: 0.2 }) or stop the stored control and then set scale:1) so repeated
start() calls don't stack and stopping returns the plus to its neutral scale.
- Around line 9-10: The hover handler calls the async start() without handling
rejections, which can cause unhandled promise rejections; update the
onHoverStart invocation (and the handleHoverStart function) to handle errors by
calling start with void and a catch (e.g., void start().catch(() => {})) or wrap
the call in an async try/catch, ensuring any rejection from start() is
swallowed; locate the start function and the handleHoverStart/onHoverStart usage
in AlarmClockPlusIcon to apply the change.

In @public/r/washing-machine-icon.json:
- Line 10: The stop() function triggers a reset animation for ".drum-inner" but
does not track its returned control, so it can't be stopped later; modify stop()
to push the animate(...) return value into animationControls.current (after
first stopping/clearing existing controls as done in start()) so the reset
animation is tracked and can be cancelled if stop() or start() is called again —
update the animationControls.current usage in stop() to mirror how start()
stores its animate(...) control.
🧹 Nitpick comments (19)
public/r/docker-icon.json (2)

10-10: Add dependency array to useImperativeHandle.

The useImperativeHandle hook is missing its dependency array (third argument). Without it, the imperative handle object is recreated on every render, which is inefficient. Since animate from useAnimate() and animationControls (a ref) have stable references, an empty dependency array is appropriate.

♻️ Suggested fix

In the embedded component code, update the useImperativeHandle call:

-    useImperativeHandle(ref, () => ({
-      startAnimation: start,
-      stopAnimation: stop,
-    }));
+    useImperativeHandle(ref, () => ({
+      startAnimation: start,
+      stopAnimation: stop,
+    }), []);

10-10: Remove unnecessary async keyword from start function.

The start function is declared as async but doesn't use await. The animate() calls return animation controls synchronously; the function doesn't need to be async.

♻️ Suggested fix
-    const start = async () => {
+    const start = () => {
icons/mouse-pointer-2-icon.tsx (1)

33-38: Consider tracking the reset animation to avoid potential visual glitch.

The reset animation on line 37 isn't tracked in animationControls. If start() is called immediately after stop() (within 0.3s), both animations will briefly compete for the same x/y properties, potentially causing a minor visual flicker.

♻️ Suggested improvement
 const stop = () => {
   animationControls.current.forEach((control) => control.stop());
   animationControls.current = [];

-  animate(".pointer", { x: 0, y: 0 }, { duration: 0.3 });
+  animationControls.current.push(
+    animate(".pointer", { x: 0, y: 0 }, { duration: 0.3 })
+  );
 };

This ensures the reset animation is cancelled if start() is triggered before it completes.

public/r/ambulance-icon.json (1)

10-10: AI summary inconsistency: .plus animation handle is not stored.

The AI summary states that the plus animation handle is "stored as well," but the actual code only stores the .ambulance animation handle:

animationControls.current.push(
  animate(".ambulance", ...),  // stored
);
animate(".plus", ...);  // NOT stored

This is acceptable since the .plus animation is finite (no repeat: Infinity), but the summary is misleading.

Additionally, the start function is marked async but contains no await expressions—the keyword can be removed.

Optional: Remove unnecessary `async`

In the embedded component code, change:

-    const start = async () => {
+    const start = () => {
icons/spotify-icon.tsx (2)

13-43: Remove unnecessary async keyword.

The start function is marked async but contains no await expressions. The animate calls return synchronously (the animation handle, not a promise that resolves when complete).

♻️ Suggested fix
-    const start = async () => {
+    const start = () => {

55-58: Consider adding a dependency array to useImperativeHandle.

Without a dependency array, the handle object is recreated on every render. Since start and stop are stable (they only reference animate and animationControls.current which don't change identity), an empty dependency array would prevent unnecessary handle updates.

♻️ Suggested fix
     useImperativeHandle(ref, () => ({
       startAnimation: start,
       stopAnimation: stop,
-    }));
+    }), []);

Alternatively, wrap start and stop with useCallback if their dependencies change across renders.

public/r/qrcode-icon.json (2)

10-10: Add cleanup for animations on component unmount.

The component tracks infinite animations in animationControls.current but doesn't clean them up when unmounting. While motion/framer-motion typically handles cleanup automatically, it's best practice to explicitly stop tracked animations in a useEffect cleanup function to prevent potential memory leaks or errors if the component unmounts mid-animation.

♻️ Suggested cleanup implementation

Add this cleanup effect after the useImperativeHandle call in the component:

useEffect(() => {
  return () => {
    animationControls.current.forEach((control) => control.stop());
    animationControls.current = [];
  };
}, []);

10-10: The animation control refactor looks solid.

The new pattern using animationControls.current to track animation handles is a good improvement for managing multiple animations, especially the infinite qr-scan loop. The implementation correctly:

  • Stops existing animations before starting new ones
  • Sequences animations with appropriate awaits
  • Exposes a clean imperative API via useImperativeHandle
  • Integrates well with hover interactions

The stop() function applies a uniform property reset ({ opacity: 1, pathLength: 1, scale: 1, y: 0 }) to all animated elements, even though not all elements use all properties. This is harmless (motion ignores irrelevant properties) and keeps the code simple.

Note: For pedantic precision, you could reset only the properties each element class actually animates, but the current approach prioritizes simplicity.

public/r/brand-vercel-icon.json (1)

10-10: Consider adding animation control tracking for consistency and to handle rapid hover.

Unlike other icons in this PR, BrandVercelIcon doesn't use the animationControls ref pattern. Rapid hover in/out could trigger overlapping animations since start/stop don't cancel in-progress animations.

Additionally, rotateY without a CSS perspective property on the parent may not produce a visible 3D flip effect—the rotation will appear as a horizontal scale rather than a flip.

🔧 Suggested improvement for consistency
-import { forwardRef, useImperativeHandle } from "react";
+import { forwardRef, useImperativeHandle, useRef } from "react";
 import type { AnimatedIconHandle, AnimatedIconProps } from "./types";
 import { motion, useAnimate } from "motion/react";

 const BrandVercelIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
   (
     { size = 24, color = "currentColor", strokeWidth = 2, className = "" },
     ref,
   ) => {
     const [scope, animate] = useAnimate();
+    const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);

     const start = async () => {
+      animationControls.current.forEach((control) => control.stop());
+      animationControls.current = [];
+
       await animate(
         ".triangle",
         { rotateY: 180 },
         { duration: 0.6, ease: "easeInOut" },
       );
     };

     const stop = async () => {
+      animationControls.current.forEach((control) => control.stop());
+      animationControls.current = [];
+
       await animate(
         ".triangle",
         { rotateY: -180 },
-        { duration: 0.6, ease: "easeInOut" },
+        { duration: 0.6, ease: "easeInOut" },
       );
     };
icons/vinyl-icon.tsx (1)

99-109: Minor: Consider adding transformOrigin for the center-dot element.

The vinyl-disc path has explicit transformOrigin: "50% 50%", but the center-dot path lacks it. While scale animations typically default to center, adding explicit transformOrigin would ensure consistent behavior across browsers.

Suggested addition
         {/* Center dot */}
         <motion.path
           className="center-dot"
           d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"
+          style={{ transformOrigin: "12px 12px" }}
         />
icons/brand-threads-icon.tsx (1)

49-55: Consider removing the wrapper functions.

The handleHoverStart and handleHoverEnd functions simply delegate to start and stop without any additional logic. You can pass start and stop directly to the hover props.

♻️ Suggested simplification
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
       <motion.svg
         ref={scope}
-        onHoverStart={handleHoverStart}
-        onHoverEnd={handleHoverEnd}
+        onHoverStart={start}
+        onHoverEnd={stop}
         xmlns="http://www.w3.org/2000/svg"
lib/icons.ts (1)

139-181: Inconsistent naming convention in icon entries.

The new brand icon entries use hyphenated names (e.g., "brand-nextjs-icon"), while existing entries use space-separated names (e.g., "brand google icon" on line 135). Consider using a consistent format across all entries for maintainability.

If the name field is intended for display, consider using spaces consistently:

-    name: "brand-nextjs-icon",
+    name: "brand nextjs icon",

Or if hyphens are preferred, update the existing entries to match.

icons/brand-react-icon.tsx (1)

12-49: Consider tracking parallel animations for proper cleanup.

The start() function launches multiple animations without awaiting them (lines 19-21, 24-48), creating a fire-and-forget pattern for parallel execution. While this achieves the desired simultaneous animation effect, these animations are not tracked and cannot be stopped if stop() is called before they complete.

Additionally, start() is async but AnimatedIconHandle declares startAnimation: () => void, creating a type mismatch.

♻️ Proposed refactor

Track animation controls similar to the washing-machine-icon pattern:

+const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);
+
 const start = async () => {
+  // Stop any existing animations
+  animationControls.current.forEach((control) => control.stop());
+  animationControls.current = [];
+
   await animate(
     ".center-dot",
     { scale: [1, 1.4, 1.2] },
     { duration: 0.4, ease: [0.34, 1.56, 0.64, 1] },
   );

-  animate(".orbit-1", { rotate: 360 }, { duration: 1, ease: "linear" });
-  animate(".orbit-2", { rotate: -360 }, { duration: 2, ease: "linear" });
-  animate(".orbit-3", { rotate: 360 }, { duration: 4, ease: "linear" });
+  animationControls.current.push(
+    animate(".orbit-1", { rotate: 360 }, { duration: 1, ease: "linear" }),
+  );
+  animationControls.current.push(
+    animate(".orbit-2", { rotate: -360 }, { duration: 2, ease: "linear" }),
+  );
+  animationControls.current.push(
+    animate(".orbit-3", { rotate: 360 }, { duration: 4, ease: "linear" }),
+  );

   // Track remaining animations similarly...
 };

 const stop = async () => {
+  animationControls.current.forEach((control) => control.stop());
+  animationControls.current = [];
+
   // Then proceed with reset animations...
 };

Also consider updating AnimatedIconHandle to reflect async signatures:

export interface AnimatedIconHandle {
  startAnimation: () => void | Promise<void>;
  stopAnimation: () => void | Promise<void>;
}
icons/alarm-clock-plus-icon.tsx (1)

52-58: Consider awaiting or tracking reset animations for cleaner state management.

The reset animations on lines 56-57 are not tracked or awaited. If stop() is called followed immediately by start(), these reset animations might still be running when new animations begin, potentially causing brief conflicts.

🛡️ Optional defensive improvement
 const stop = () => {
   animationControls.current.forEach((control) => control.stop());
   animationControls.current = [];

-  animate(".clock", { y: 0, x: 0 }, { duration: 0.2 });
-  animate(".bells", { y: 0, x: 0 }, { duration: 0.2 });
+  animate(".clock", { y: 0, x: 0 }, { duration: 0 });
+  animate(".bells", { y: 0, x: 0 }, { duration: 0 });
 };

Setting duration: 0 ensures instant reset, eliminating potential overlap with subsequent start() calls.

icons/docker-icon.tsx (2)

13-36: start is async but never awaits; also the .dot animation isn’t tracked/stoppable.
Make it sync unless you intentionally need a Promise, and consider tracking .dot too to avoid overlapping “pulse” animations on rapid re-hover.

Suggested diff
- const start = async () => {
-   animationControls.current.forEach((control) => control.stop());
-   animationControls.current = [];
+ const start = () => {
+   animationControls.current.forEach((control) => control.stop());
+   animationControls.current = [];

    animationControls.current.push(
      animate(
        ".whale",
        { x: [0, 2, 0] },
        { duration: 1.2, ease: "easeInOut", repeat: Infinity },
      ),
    );

    animationControls.current.push(
      animate(
        ".containers",
        { y: [0, -1, 0] },
        { duration: 0.8, ease: "easeInOut", repeat: Infinity },
      ),
    );

-   animate(
+   animationControls.current.push(
+     animate(
        ".dot",
        { scale: [1, 1.3, 1] },
        { duration: 0.5, ease: "easeInOut" },
-   );
+     ),
+   );
  };

38-45: Consider unmount cleanup to prevent “orphaned” infinite animations.
If the icon unmounts while hovered (route change, conditional render), the repeating animations may keep running unless you stop them in an effect cleanup.

Suggested diff (adds cleanup)
-import { forwardRef, useImperativeHandle, useRef } from "react";
+import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";

  const stop = () => {
    animationControls.current.forEach((control) => control.stop());
    animationControls.current = [];

    animate(".whale", { x: 0 }, { duration: 0.3 });
    animate(".containers", { y: 0 }, { duration: 0.3 });
    animate(".dot", { scale: 1 }, { duration: 0.3 });
  };

+ useEffect(() => () => stop(), []);
icons/ambulance-icon.tsx (1)

11-16: Use AnimationPlaybackControls from "motion/react" instead of ReturnType<typeof animate> for better type clarity and safety.

The animate function in motion v12.23.26 returns AnimationPlaybackControls (or AnimationPlaybackControlsWithThen), which is exported from "motion/react". Using the concrete type directly—rather than inferring it via ReturnType—avoids potential type widening from overloads and makes the intent clearer:

import type { AnimationPlaybackControls } from "motion/react";

const animationControls = useRef<AnimationPlaybackControls[]>([]);
public/r/alarm-clock-plus-icon.json (2)

9-10: “only when NOT controlled” comment doesn’t match behavior—add an explicit guard/prop.
onHoverStart/onHoverEnd always fire; if a parent is also driving startAnimation/stopAnimation, the two can fight. Consider a prop like hoverEnabled?: boolean (default true) and gate the handlers.


9-10: Add unmount cleanup for infinite animations (bells/clock repeat forever).
If the component unmounts while repeating, stopping controls in an effect cleanup avoids orphaned animations.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee3b94 and 81495ae.

📒 Files selected for processing (35)
  • app/layout.tsx
  • icons/accessibility-icon.tsx
  • icons/alarm-clock-plus-icon.tsx
  • icons/ambulance-icon.tsx
  • icons/brand-railway-icon.tsx
  • icons/brand-react-icon.tsx
  • icons/brand-react-native-icon.tsx
  • icons/brand-stripe-icon.tsx
  • icons/brand-threads-icon.tsx
  • icons/brand-vercel-icon.tsx
  • icons/docker-icon.tsx
  • icons/globe-icon.tsx
  • icons/mouse-pointer-2-icon.tsx
  • icons/qrcode-icon.tsx
  • icons/spotify-icon.tsx
  • icons/vinyl-icon.tsx
  • icons/washing-machine-icon.tsx
  • lib/icon-names.ts
  • lib/icons.ts
  • public/r/accessibility-icon.json
  • public/r/alarm-clock-plus-icon.json
  • public/r/ambulance-icon.json
  • public/r/brand-railway-icon.json
  • public/r/brand-react-icon.json
  • public/r/brand-react-native-icon.json
  • public/r/brand-stripe-icon.json
  • public/r/brand-threads-icon.json
  • public/r/brand-vercel-icon.json
  • public/r/docker-icon.json
  • public/r/globe-icon.json
  • public/r/mouse-pointer-2-icon.json
  • public/r/qrcode-icon.json
  • public/r/spotify-icon.json
  • public/r/vinyl-icon.json
  • public/r/washing-machine-icon.json
💤 Files with no reviewable changes (1)
  • app/layout.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • icons/brand-vercel-icon.tsx
  • public/r/brand-railway-icon.json
  • public/r/brand-threads-icon.json
🔇 Additional comments (31)
icons/accessibility-icon.tsx (3)

1-1: LGTM! Solid foundation for multi-animation management.

The addition of useRef and animationControls ref establishes a robust pattern for tracking multiple concurrent animations, enabling proper cleanup and preventing animation leaks.

Also applies to: 11-11


14-23: Good: Wheel animation is properly tracked.

The wheel animation is correctly added to animationControls.current, allowing it to be stopped when needed. The cleanup of existing animations before starting new ones prevents animation accumulation.


31-37: Stop logic is correct; reset animations intentionally untracked.

The forEach pattern properly stops all tracked animations and clears the array. The subsequent reset animations (lines 35-36) are not tracked, which is acceptable since they're short-lived cleanup animations that restore initial state.

public/r/docker-icon.json (1)

14-17: LGTM!

The types file is well-structured with clear JSDoc comments documenting each property. The AnimatedIconHandle interface properly defines the animation control API.

icons/mouse-pointer-2-icon.tsx (3)

1-11: LGTM on the setup and animation control tracking pattern.

The animationControls ref pattern for tracking animation handles is a clean approach that allows proper cleanup of running animations.


13-31: Start function implementation is correct.

The cleanup-before-start pattern prevents animation accumulation. Minor observation: async keyword on line 13 is unnecessary since there's no await, but it doesn't cause any issues.


40-67: Imperative handle and rendering look good.

The useImperativeHandle correctly exposes startAnimation and stopAnimation as per AnimatedIconHandle, and the hover handlers are properly wired to the animation functions.

public/r/ambulance-icon.json (1)

14-16: LGTM!

The type definitions are clean and provide a clear contract for the animated icon components.

public/r/spotify-icon.json (1)

1-20: LGTM!

Registry file structure follows shadcn conventions correctly. Dependencies are properly declared, and the embedded content aligns with the source file.

icons/spotify-icon.tsx (3)

1-11: LGTM!

Proper use of useRef to store animation controls with correct type inference via ReturnType<typeof animate>.


45-53: LGTM!

Clean teardown pattern - stops tracked infinite animations before resetting all elements to their initial state with a smooth transition.


60-91: LGTM!

Clean JSX structure with proper hover handlers for automatic animation. The transformOrigin: "center" on the circle ensures correct scale animation behavior.

public/r/mouse-pointer-2-icon.json (1)

10-10: LGTM!

The animation control pattern is well-implemented:

  • Properly stops and clears existing animations before starting new ones
  • Correctly resets position on stop with a smooth transition
  • Imperative handle exposes clean API for external control
icons/globe-icon.tsx (1)

1-31: LGTM!

The refactor to use animationControls ref for managing multiple animation handles is well-structured:

  • Properly clears existing animations before starting new ones to prevent animation overlap
  • Reset animation in stop() is correctly not tracked since it's a one-shot transition
  • Consistent with the broader pattern established across other icon components in this PR
public/r/vinyl-icon.json (1)

10-10: LGTM!

The vinyl icon correctly manages two concurrent infinite animations (disc rotation and center-dot pulse) using the centralized animationControls pattern. Both are properly stopped and reset on hover end.

icons/qrcode-icon.tsx (1)

1-63: LGTM!

The animation control pattern is correctly applied:

  • Only the infinite qr-scan animation is tracked in animationControls since it needs explicit cleanup
  • One-shot animations (corner-rect reveal, inner-element fade-in, center-dot pop) don't require tracking as they complete naturally
  • The stop() function properly resets all animated elements to their resting state
icons/vinyl-icon.tsx (1)

1-73: LGTM!

The animation control implementation is clean and correct:

  • Both infinite animations (disc rotation and center-dot pulse) are properly tracked
  • Animations are stopped and cleared before restart to prevent overlap
  • Reset animations use appropriate easing for smooth transitions
public/r/globe-icon.json (1)

1-20: LGTM!

The animation control pattern is well-implemented:

  • Properly clears existing animation controls before starting new ones
  • Uses useImperativeHandle to expose the animation API consistently with other icons
  • The stop function correctly resets rotation state after stopping tracked animations
icons/brand-react-native-icon.tsx (2)

13-72: LGTM!

The animation orchestration is well-structured with proper cleanup. The entrance animation awaits before starting infinite loops, and the animationControls ref correctly tracks all continuous animations for cleanup.


74-88: LGTM!

The stop function properly clears all tracked infinite animations and fires brief reset animations to restore the initial state.

icons/washing-machine-icon.tsx (1)

11-48: LGTM!

The refactor to the animationControls ref pattern is clean and consistent with the other animated icons in this PR. Properly stops existing animations before starting new ones.

lib/icon-names.ts (1)

26-44: LGTM!

The brand icon names are added with consistent brand-* naming convention and the array remains sorted.

public/r/brand-stripe-icon.json (1)

1-20: LGTM!

The implementation properly uses useId() to generate unique clip-path IDs, preventing conflicts when multiple icon instances are rendered. The fill animation effect is clean and the API exposure is consistent with other brand icons.

icons/brand-railway-icon.tsx (1)

1-1: LGTM! Component renamed to brand-prefixed convention.

The renaming from RailwayIcon to BrandRailwayIcon aligns with the broader PR objective of standardizing brand icon naming. The removal of useCallback in favor of direct function declarations is appropriate given the ref-based animation control pattern.

Also applies to: 5-5, 67-68

icons/brand-stripe-icon.tsx (1)

11-12: Excellent: Unique clipPath ID prevents conflicts.

Using useId() and sanitizing it to generate unique clipPath IDs is a solid pattern that prevents conflicts when multiple BrandStripeIcon instances render simultaneously. The colon sanitization (replace(/:/g, "")) ensures valid SVG IDs.

icons/brand-react-icon.tsx (1)

84-127: LGTM! Well-structured React logo animation.

The SVG structure with three orbit groups and a center dot effectively represents the React logo, and the use of inline style props for transform origins ensures correct rotation behavior. The class-based targeting (.orbit-1, .orbit-2, .orbit-3, .center-dot) cleanly separates animation targets.

public/r/brand-react-icon.json (1)

1-20: Registry structure is well-formed.

The registry item correctly declares the motion dependency, includes both the component and types files, and follows the shadcn/ui registry schema. The file structure is appropriate for UI component distribution.

icons/alarm-clock-plus-icon.tsx (2)

1-1: LGTM! Clean animation control setup.

The addition of useRef and the animationControls ref with proper typing establishes a solid foundation for managing multiple animation handles.

Also applies to: 11-11


1-117: No actionable concerns identified with the animation control implementation in this file. The pattern correctly tracks looping animations in animationControls and properly clears them on stop. While animation patterns do vary across the codebase, this inconsistency should be addressed as a separate refactoring task, not within an individual file review.

icons/docker-icon.tsx (1)

1-1: No changes needed. The animate() function from motion v12.23.26 always returns AnimationPlaybackControls, which includes a stop(): void method. The type is guaranteed by the library, so ReturnType<typeof animate> safely includes .stop() and the current implementation is already type-safe. The proposed defensive tightening or guards are unnecessary.

Likely an incorrect or invalid review comment.

icons/ambulance-icon.tsx (1)

10-23: Good: stopping previous handles before starting prevents “stacked” infinite animations.
This is a solid guard against repeated hover-starts creating multiple concurrent .ambulance loops.

Comment on lines +9 to +33
const start = async () => {
await animate(
".railway-inner",
{ opacity: 0 },
{ duration: 0.6, ease: "linear" },
);
}, [animate, scope]);
await animate(
".railway-lower",
{ x: [0, -3, 3, 0] },
{ duration: 0.6, ease: "easeInOut" },
);
};

const stop = useCallback(() => {
animate(scope.current, { x: 0, y: 0 }, { duration: 0.15 });
}, [animate, scope]);
const stop = async () => {
await animate(
".railway-inner",
{ opacity: 1 },
{ duration: 0.2, ease: "linear" },
);
};

useImperativeHandle(
ref,
() => ({
startAnimation: start,
stopAnimation: stop,
}),
[start, stop],
);
useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix type mismatch: async functions declared as returning void.

The start and stop functions are async (returning Promise<void>), but AnimatedIconHandle declares startAnimation and stopAnimation as () => void. This type mismatch can lead to unhandled promise rejections and incorrect usage by callers who won't know to await these methods.

🔧 Proposed fix

Option 1 (Recommended): Update the AnimatedIconHandle interface in icons/types.ts:

 export interface AnimatedIconHandle {
-  startAnimation: () => void;
-  stopAnimation: () => void;
+  startAnimation: () => void | Promise<void>;
+  stopAnimation: () => void | Promise<void>;
 }

Option 2: Remove async from the functions if synchronous behavior is sufficient:

-const start = async () => {
-  await animate(
+const start = () => {
+  animate(
     ".railway-inner",
     { opacity: 0 },
     { duration: 0.6, ease: "linear" },
   );
-  await animate(
+  animate(
     ".railway-lower",
     { x: [0, -3, 3, 0] },
     { duration: 0.6, ease: "easeInOut" },
   );
 };

-const stop = async () => {
-  await animate(
+const stop = () => {
+  animate(
     ".railway-inner",
     { opacity: 1 },
     { duration: 0.2, ease: "linear" },
   );
 };
📝 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 start = async () => {
await animate(
".railway-inner",
{ opacity: 0 },
{ duration: 0.6, ease: "linear" },
);
}, [animate, scope]);
await animate(
".railway-lower",
{ x: [0, -3, 3, 0] },
{ duration: 0.6, ease: "easeInOut" },
);
};
const stop = useCallback(() => {
animate(scope.current, { x: 0, y: 0 }, { duration: 0.15 });
}, [animate, scope]);
const stop = async () => {
await animate(
".railway-inner",
{ opacity: 1 },
{ duration: 0.2, ease: "linear" },
);
};
useImperativeHandle(
ref,
() => ({
startAnimation: start,
stopAnimation: stop,
}),
[start, stop],
);
useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
const start = () => {
animate(
".railway-inner",
{ opacity: 0 },
{ duration: 0.6, ease: "linear" },
);
animate(
".railway-lower",
{ x: [0, -3, 3, 0] },
{ duration: 0.6, ease: "easeInOut" },
);
};
const stop = () => {
animate(
".railway-inner",
{ opacity: 1 },
{ duration: 0.2, ease: "linear" },
);
};
useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
🤖 Prompt for AI Agents
In @icons/brand-railway-icon.tsx around lines 9 - 33, The exported imperative
handle methods have a type mismatch: the async functions start and stop
(Promise<void>) are being exposed as startAnimation and stopAnimation typed as
() => void; update the AnimatedIconHandle interface in icons/types.ts so
startAnimation and stopAnimation are typed as () => Promise<void> (or change the
handle to return the existing Promise) and ensure useImperativeHandle returns
the start and stop functions as-is; alternatively, if you prefer synchronous
methods, remove async/await from the start and stop functions and keep the
interface as () => void, but the recommended fix is to change AnimatedIconHandle
to use Promise<void> for both methods.

Comment on lines +14 to +31
const start = async () => {
await animate(
".fill-rect",
{
y: [-2, 26],
},
{ duration: 1.4, ease: "easeInOut" },
);
};

const stop = () => {
animate(".fill-rect", { y: -2 }, { duration: 0.3, ease: "easeOut" });
};

useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix type mismatch: start() is async but declared as returning void.

The start function is async (line 14), but the AnimatedIconHandle interface declares startAnimation: () => void. This creates a type safety issue and inconsistent behavior where startAnimation returns a Promise but callers expect void.

Additionally, there's inconsistency within this component: start() is async while stop() (line 24) is not, creating an asymmetric API.

🔧 Proposed fix

Option 1: Make both functions synchronous (fire-and-forget animations):

-const start = async () => {
-  await animate(
+const start = () => {
+  animate(
     ".fill-rect",
     {
       y: [-2, 26],
     },
     { duration: 1.4, ease: "easeInOut" },
   );
 };

Option 2: Update the AnimatedIconHandle interface to allow Promise returns (apply across all icons):

 export interface AnimatedIconHandle {
-  startAnimation: () => void;
-  stopAnimation: () => void;
+  startAnimation: () => void | Promise<void>;
+  stopAnimation: () => void | Promise<void>;
 }
📝 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 start = async () => {
await animate(
".fill-rect",
{
y: [-2, 26],
},
{ duration: 1.4, ease: "easeInOut" },
);
};
const stop = () => {
animate(".fill-rect", { y: -2 }, { duration: 0.3, ease: "easeOut" });
};
useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
const start = () => {
animate(
".fill-rect",
{
y: [-2, 26],
},
{ duration: 1.4, ease: "easeInOut" },
);
};
const stop = () => {
animate(".fill-rect", { y: -2 }, { duration: 0.3, ease: "easeOut" });
};
useImperativeHandle(ref, () => ({
startAnimation: start,
stopAnimation: stop,
}));
🤖 Prompt for AI Agents
In @icons/brand-stripe-icon.tsx around lines 14 - 31, The start function is
declared async (start) but AnimatedIconHandle defines startAnimation: () =>
void, causing a type mismatch and asymmetry with stop; either make both
functions synchronous by removing async/await from start and calling animate
without awaiting so start returns void (and keep stop as-is), or change the
AnimatedIconHandle signature to startAnimation: () => Promise<void> and update
all icon components (and any callers) to return/handle Promise<void> for both
start and stop to keep the API consistent; ensure useImperativeHandle returns
the matching types for startAnimation and stopAnimation and update any related
types/usages.

Comment on lines +12 to +35
const start = async () => {
// Reset
animate(scope.current, { scale: 1, rotate: 0 });
animate(".animated-path", { pathLength: 1, opacity: 1 });

// Animation
animate(
".animated-path",
{ pathLength: [0, 1], opacity: [0, 1] },
{ duration: 1, ease: "easeInOut" },
);

await animate(
scope.current,
{
scale: [0.5, 1.1, 1],
rotate: [180, -10, 0],
},
{
duration: 1,
ease: [0.34, 1.56, 0.64, 1],
},
);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider adding animation cleanup for rapid hover interactions.

Unlike other brand icons in this PR, this component doesn't track and cancel existing animations before starting new ones. If a user rapidly hovers in and out, overlapping animations could occur, causing visual glitches.

🐛 Suggested fix
+import { forwardRef, useImperativeHandle, useRef } from "react";
-import { forwardRef, useImperativeHandle } from "react";
 import type { AnimatedIconHandle, AnimatedIconProps } from "./types";
 import { motion, useAnimate } from "motion/react";

 const BrandThreadsIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
   (
     { size = 24, color = "currentColor", strokeWidth = 2, className = "" },
     ref,
   ) => {
     const [scope, animate] = useAnimate();
+    const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);

     const start = async () => {
+      animationControls.current.forEach((control) => control.stop());
+      animationControls.current = [];
+
       // Reset
       animate(scope.current, { scale: 1, rotate: 0 });
       animate(".animated-path", { pathLength: 1, opacity: 1 });

       // Animation
-      animate(
+      animationControls.current.push(animate(
         ".animated-path",
         { pathLength: [0, 1], opacity: [0, 1] },
         { duration: 1, ease: "easeInOut" },
-      );
+      ));

-      await animate(
+      animationControls.current.push(animate(
         scope.current,
         {
           scale: [0.5, 1.1, 1],
           rotate: [180, -10, 0],
         },
         {
           duration: 1,
           ease: [0.34, 1.56, 0.64, 1],
         },
-      );
+      ));
     };

     const stop = () => {
+      animationControls.current.forEach((control) => control.stop());
+      animationControls.current = [];
       animate(scope.current, { scale: 1, rotate: 0 });
       animate(".animated-path", { pathLength: 1, opacity: 1 });
     };
🤖 Prompt for AI Agents
In @icons/brand-threads-icon.tsx around lines 12 - 35, The start function can
spawn overlapping animations on rapid hover; before beginning new animations in
start, cancel/stop any existing animations created for scope.current and
".animated-path" (store animation handles/controls returned by animate in a ref
like currentAnimationRef), then start the new animations and save their handles;
also ensure you stop those handles on unmount (cleanup in useEffect) so
scope.current animations are always cancelled before starting new ones.

{
"path": "icons/accessibility-icon.tsx",
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AccessibilityIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n animate(\n \".wheel\",\n { rotate: [0, 360] },\n { duration: 1, ease: \"easeInOut\", repeat: Infinity },\n );\n animate(\n \".person\",\n { y: [0, -2, 0] },\n { duration: 0.6, ease: \"easeInOut\" },\n );\n };\n\n const stop = () => {\n animate(\".wheel\", { rotate: 0 }, { duration: 0.3 });\n animate(\".person\", { y: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className} `}\n >\n <motion.circle className=\"person\" cx=\"16\" cy=\"4\" r=\"1\" />\n <motion.path className=\"person\" d=\"m18 19 1-7-6 1\" />\n <motion.path className=\"person\" d=\"m5 8 3-3 5.5 3-2.36 3.5\" />\n <motion.g className=\"wheel\" style={{ transformOrigin: \"8.5px 17.5px\" }}>\n <path d=\"M4.24 14.5a5 5 0 0 0 6.88 6\" />\n <path d=\"M13.76 17.5a5 5 0 0 0-6.88-6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAccessibilityIcon.displayName = \"AccessibilityIcon\";\n\nexport default AccessibilityIcon;\n",
"content": "import { forwardRef, useImperativeHandle, useRef } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AccessibilityIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);\n\n const start = async () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animationControls.current.push(\n animate(\n \".wheel\",\n { rotate: [0, 360] },\n { duration: 1, ease: \"easeInOut\", repeat: Infinity },\n ),\n );\n animate(\n \".person\",\n { y: [0, -2, 0] },\n { duration: 0.6, ease: \"easeInOut\" },\n );\n };\n\n const stop = () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animate(\".wheel\", { rotate: 0 }, { duration: 0.3 });\n animate(\".person\", { y: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className} `}\n >\n <motion.circle className=\"person\" cx=\"16\" cy=\"4\" r=\"1\" />\n <motion.path className=\"person\" d=\"m18 19 1-7-6 1\" />\n <motion.path className=\"person\" d=\"m5 8 3-3 5.5 3-2.36 3.5\" />\n <motion.g className=\"wheel\" style={{ transformOrigin: \"8.5px 17.5px\" }}>\n <path d=\"M4.24 14.5a5 5 0 0 0 6.88 6\" />\n <path d=\"M13.76 17.5a5 5 0 0 0-6.88-6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAccessibilityIcon.displayName = \"AccessibilityIcon\";\n\nexport default AccessibilityIcon;\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Store the person animation control to prevent overlapping animations.

The start() function tracks the wheel animation control but doesn't store the person animation control in animationControls.current. This means:

  • When stop() is called, the person animation won't be stopped (it's not in the array)
  • If start() is called multiple times (e.g., rapid hover), multiple person animations will overlap and continue running
  • This causes visual glitches and unnecessary performance overhead
🔧 Proposed fix

Store both animation controls consistently:

       animationControls.current.push(
         animate(
           ".wheel",
           { rotate: [0, 360] },
           { duration: 1, ease: "easeInOut", repeat: Infinity },
         ),
       );
-      animate(
+      animationControls.current.push(
+        animate(
-        ".person",
-        { y: [0, -2, 0] },
-        { duration: 0.6, ease: "easeInOut" },
-      );
+          ".person",
+          { y: [0, -2, 0] },
+          { duration: 0.6, ease: "easeInOut" },
+        ),
+      );
🤖 Prompt for AI Agents
In @public/r/accessibility-icon.json at line 10, The start() function currently
pushes only the wheel animation control into animationControls.current but calls
animate(".person", ...) without storing its returned control, so overlapping
person animations won't be stopped; update start() to capture the return value
of animate(".person", { y: [...] }, { ... }) and push that control into
animationControls.current (alongside the wheel control) so stop() can stop all
running animations; reference start(), stop(), animationControls.current and the
animate(".person", ...) call when making this change.

Comment on lines 9 to +10
"path": "icons/alarm-clock-plus-icon.tsx",
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AlarmClockPlusIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n animate(\n \".clock\",\n {\n y: -1.5,\n x: [-1, 1, -1, 1, -1, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n );\n\n animate(\n \".bells\",\n {\n y: -2.5,\n x: [-2, 2, -2, 2, -2, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n );\n\n await animate(\n \".plus\",\n { scale: [1, 1.2, 1] },\n { duration: 0.4, ease: \"easeOut\" },\n );\n };\n\n const stop = () => {\n animate(\".clock\", { y: 0, x: 0 }, { duration: 0.2 });\n animate(\".bells\", { y: 0, x: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n // 🖱 hover logic (only when NOT controlled)\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{ overflow: \"visible\" }}\n >\n <motion.circle className=\"clock\" cx=\"12\" cy=\"13\" r=\"8\" />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"3.5px 4.5px\" }}\n d=\"M5 3 2 6\"\n />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"20.5px 4.5px\" }}\n d=\"m22 6-3-3\"\n />\n <motion.path className=\"clock\" d=\"M6.38 18.7 4 21\" />\n <motion.path className=\"clock\" d=\"M17.64 18.67 20 21\" />\n <motion.g className=\"plus\" style={{ transformOrigin: \"12px 13px\" }}>\n <path d=\"M12 10v6\" />\n <path d=\"M9 13h6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAlarmClockPlusIcon.displayName = \"AlarmClockPlusIcon\";\n\nexport default AlarmClockPlusIcon;\n",
"content": "import { forwardRef, useImperativeHandle, useRef } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AlarmClockPlusIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);\n\n const start = async () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animationControls.current.push(\n animate(\n \".clock\",\n {\n y: -1.5,\n x: [-1, 1, -1, 1, -1, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n animationControls.current.push(\n animate(\n \".bells\",\n {\n y: -2.5,\n x: [-2, 2, -2, 2, -2, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n await animate(\n \".plus\",\n { scale: [1, 1.2, 1] },\n { duration: 0.4, ease: \"easeOut\" },\n );\n };\n\n const stop = () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animate(\".clock\", { y: 0, x: 0 }, { duration: 0.2 });\n animate(\".bells\", { y: 0, x: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n // 🖱 hover logic (only when NOT controlled)\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{ overflow: \"visible\" }}\n >\n <motion.circle className=\"clock\" cx=\"12\" cy=\"13\" r=\"8\" />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"3.5px 4.5px\" }}\n d=\"M5 3 2 6\"\n />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"20.5px 4.5px\" }}\n d=\"m22 6-3-3\"\n />\n <motion.path className=\"clock\" d=\"M6.38 18.7 4 21\" />\n <motion.path className=\"clock\" d=\"M17.64 18.67 20 21\" />\n <motion.g className=\"plus\" style={{ transformOrigin: \"12px 13px\" }}>\n <path d=\"M12 10v6\" />\n <path d=\"M9 13h6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAlarmClockPlusIcon.displayName = \"AlarmClockPlusIcon\";\n\nexport default AlarmClockPlusIcon;\n",
Copy link
Copy Markdown
Contributor

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:

# Find package.json to check the motion/react version
find . -name "package.json" -type f | head -5

Repository: itshover/itshover

Length of output: 75


🏁 Script executed:

# Search for other usages of useAnimate and animate in the codebase to understand patterns
rg "useAnimate|animate\(" --type tsx --type ts -B2 -A2 | head -80

Repository: itshover/itshover

Length of output: 88


🏁 Script executed:

# Check if there are any type definitions or documentation about motion/react in the repo
find . -name "*.d.ts" -o -name "*types*" | grep -i motion

Repository: itshover/itshover

Length of output: 43


🏁 Script executed:

cat package.json | head -50

Repository: itshover/itshover

Length of output: 1545


🏁 Script executed:

# Search for useAnimate and animate patterns in the codebase
rg "useAnimate" -A 5 -B 1 | head -100

Repository: itshover/itshover

Length of output: 45182


🏁 Script executed:

# Search for animate\( calls to see usage patterns
rg "animate\(" -A 3 | head -100

Repository: itshover/itshover

Length of output: 4799


🏁 Script executed:

# Let's check the actual file being reviewed
cat public/r/alarm-clock-plus-icon.json | head -20

Repository: itshover/itshover

Length of output: 4332


🏁 Script executed:

# Check if there are any TypeScript type issues or motion type definitions in the repo
rg "ReturnType.*animate" | head -20

Repository: itshover/itshover

Length of output: 39539


🏁 Script executed:

# Search for motion documentation or test files
find . -name "*.md" -type f | xargs grep -l "motion" | head -5

Repository: itshover/itshover

Length of output: 108


🏁 Script executed:

# Check ARCHITECTURE and CONTRIBUTING for guidance
grep -A 10 -B 5 "await animate\|animate(" ARCHITECTURE.md | head -40

Repository: itshover/itshover

Length of output: 693


🏁 Script executed:

# Look for any type errors or how animate return is used elsewhere
rg "animate\(" icons/ -A 2 | grep -E "animate\(|control|stop" | head -40

Repository: itshover/itshover

Length of output: 2480


Track and reset the .plus animation in the control array.

The .plus animation is neither tracked in animationControls.current nor reset in stop(), unlike .clock and .bells. This causes two issues:

  1. Stacked animations on repeated calls: Calling start() multiple times (e.g., rapid hover or programmatic calls) launches new .plus scale animations without stopping the previous ones, causing overlapping visual effects.
  2. Incomplete reset: stop() doesn't reset .plus scale to 1, so if stopped mid-animation, it remains at 1.2.

Store the .plus control in the array and reset its scale in stop() to match the pattern used for other tracked animations:

Proposed fix
       await animate(
         ".plus",
         { scale: [1, 1.2, 1] },
         { duration: 0.4, ease: "easeOut" },
       );
+      animationControls.current.push(plusControl);

And in stop():

       animate(".bells", { y: 0, x: 0 }, { duration: 0.2 });
+      animate(".plus", { scale: 1 }, { duration: 0.2 });
🤖 Prompt for AI Agents
In @public/r/alarm-clock-plus-icon.json around lines 9 - 10, The .plus animation
isn't being tracked or stopped: in start() push the animate(...) call for the
".plus" tween into animationControls.current (like the pushes for ".clock" and
".bells") so it can be stopped later, and in stop() iterate/stop those controls
and also ensure you reset the plus element to scale 1 (e.g., call
animate(".plus", { scale: 1 }, { duration: 0.2 }) or stop the stored control and
then set scale:1) so repeated start() calls don't stack and stopping returns the
plus to its neutral scale.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Framer Motion animate function error handling promise rejection

💡 Result:

Short answer: animate returns a Web-Animation-like object whose finished Promise will reject if the animation is aborted/cancelled (it rejects with a DOMException/AbortError). Catch or await that promise to handle rejections and avoid unhandled-Promise errors. (motion.dev)

Example patterns:

  • .catch:
    const anim = animate(element, { opacity: 0 }, { duration: 0.5 })
    anim.finished.catch(err => {
    if (err.name === 'AbortError') { /* cancelled / }
    else { /
    other error */ }
    })

  • async/try-catch:
    try {
    await anim.finished
    // finished normally
    } catch (err) {
    if (err.name === 'AbortError') { /* cancelled / }
    else { /
    handle other errors */ }
    }

Notes:

  • The rejection behavior comes from the Web Animations API (Animation.finished rejects on cancel). If you call animation.cancel() (or the library cancels it), expect an AbortError. (developer.mozilla.org)

If you want, tell me which library/version (framer-motion vs motion) and I’ll give a minimal repro showing cancellation and handling.

Citations:


🌐 Web query:

React onHoverStart async function best practices unhandled promise rejection

💡 Result:

Short summary and best practices

Problems

  • Making a React JSX event prop (e.g. onMouseEnter/onPointerEnter/onMouseOver) an async function can return a Promise that — if not awaited/handled — produces an unhandled rejection (runtime warning) and/or linter errors (no-floating-promises / no-misused-promises). [1][2][3]

Recommended patterns

  1. Keep the JSX handler synchronous; call an async helper and handle errors there:

    • onMouseEnter={() => handleHoverStart()} with
      async function handleHoverStart() { try { await doWork(); } catch (err) { handleError(err); } }
    • This avoids returning a Promise from the event prop and makes error handling explicit. [3]
  2. If you need to call an async function inline, attach a rejection handler:

    • onMouseEnter={() => void doWork().catch(handleError)}
    • Using void satisfies some linters as an explicit “I’m intentionally not awaiting” marker, but it does not handle the rejection unless you add .catch(). [3]
  3. Use try/catch inside the async worker (preferred) or .then/.catch when chaining:

    • Always handle errors (try/catch or .catch) so the Promise won’t go unhandled. (General Promise best practice / MDN). [4]
  4. For hover-specific concerns (cancel when mouse leaves, rapid transitions):

    • Use AbortController or a cancellable token passed to the async work so you can cancel network/timers on onMouseLeave.
    • Debounce/throttle hover-triggered async work to avoid many concurrent Promises.
  5. Guard against component unmounts:

    • Track mounted state or cancel requests (AbortController) before calling setState after an awaited result to avoid updates after unmount.
  6. Lint config options (TypeScript + ESLint)

    • no-floating-promises and no-misused-promises will flag async handlers. Options: explicitly handle the Promise, prefix with void (and add .catch), or adjust rule settings if you knowingly accept floating Promises. Prefer fixing code to satisfy the rules. [2][3]
  7. App-level fallback

    • Add a global unhandledrejection handler for logging/fallback (window.addEventListener('unhandledrejection', ...)), but don’t rely on this instead of local handling. [4][1]

Minimal example

  • JSX:
    onMouseEnter={() => void hoverStart().catch(console.error)}
  • Worker:
    async function hoverStart(signal?: AbortSignal) {
    try {
    const data = await fetch(url, { signal });
    /* use data /
    } catch (err) {
    if (err.name === 'AbortError') return;
    /
    local error handling / report to monitoring */
    throw err; // or handle here
    }
    }

Sources

  • MDN: window unhandledrejection / rejectionhandled (use for global handling). [4][1]
  • typescript-eslint: no-floating-promises, no-misused-promises (rules & options). [2][3]
  • General async event handler guidance (try/catch/.catch). [5]

References
[1] MDN — rejectionhandled event.
[2] MDN — unhandledrejection event.
[3] typescript-eslint — no-floating-promises / no-misused-promises.
[4] Practical guidance: wrap awaits in try/catch or add .catch() (examples).


🏁 Script executed:

# Check if there are other similar hover handlers in the codebase
rg "onHoverStart|onHoverEnd" --type tsx --type jsx -B 2 -A 2 | head -50

Repository: itshover/itshover

Length of output: 88


Add error handling to the hover animation calls.
start() is async and calls Framer Motion's animate(), which can reject (e.g., if cancelled). Calling start() from onHoverStart without error handling leaves unhandled promise rejections. Use void start().catch(() => {}) to explicitly handle animation failures, or wrap handleHoverStart in try/catch. For a UI animation, silently catching is acceptable.

Proposed fix
     const handleHoverStart = () => {
-      start();
+      void start().catch(() => {});
     };
📝 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
"path": "icons/alarm-clock-plus-icon.tsx",
"content": "import { forwardRef, useImperativeHandle } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AlarmClockPlusIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = async () => {\n animate(\n \".clock\",\n {\n y: -1.5,\n x: [-1, 1, -1, 1, -1, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n );\n\n animate(\n \".bells\",\n {\n y: -2.5,\n x: [-2, 2, -2, 2, -2, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n );\n\n await animate(\n \".plus\",\n { scale: [1, 1.2, 1] },\n { duration: 0.4, ease: \"easeOut\" },\n );\n };\n\n const stop = () => {\n animate(\".clock\", { y: 0, x: 0 }, { duration: 0.2 });\n animate(\".bells\", { y: 0, x: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n // 🖱 hover logic (only when NOT controlled)\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{ overflow: \"visible\" }}\n >\n <motion.circle className=\"clock\" cx=\"12\" cy=\"13\" r=\"8\" />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"3.5px 4.5px\" }}\n d=\"M5 3 2 6\"\n />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"20.5px 4.5px\" }}\n d=\"m22 6-3-3\"\n />\n <motion.path className=\"clock\" d=\"M6.38 18.7 4 21\" />\n <motion.path className=\"clock\" d=\"M17.64 18.67 20 21\" />\n <motion.g className=\"plus\" style={{ transformOrigin: \"12px 13px\" }}>\n <path d=\"M12 10v6\" />\n <path d=\"M9 13h6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAlarmClockPlusIcon.displayName = \"AlarmClockPlusIcon\";\n\nexport default AlarmClockPlusIcon;\n",
"content": "import { forwardRef, useImperativeHandle, useRef } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AlarmClockPlusIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);\n\n const start = async () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animationControls.current.push(\n animate(\n \".clock\",\n {\n y: -1.5,\n x: [-1, 1, -1, 1, -1, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n animationControls.current.push(\n animate(\n \".bells\",\n {\n y: -2.5,\n x: [-2, 2, -2, 2, -2, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n await animate(\n \".plus\",\n { scale: [1, 1.2, 1] },\n { duration: 0.4, ease: \"easeOut\" },\n );\n };\n\n const stop = () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animate(\".clock\", { y: 0, x: 0 }, { duration: 0.2 });\n animate(\".bells\", { y: 0, x: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n // 🖱 hover logic (only when NOT controlled)\n const handleHoverStart = () => {\n start();\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{ overflow: \"visible\" }}\n >\n <motion.circle className=\"clock\" cx=\"12\" cy=\"13\" r=\"8\" />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"3.5px 4.5px\" }}\n d=\"M5 3 2 6\"\n />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"20.5px 4.5px\" }}\n d=\"m22 6-3-3\"\n />\n <motion.path className=\"clock\" d=\"M6.38 18.7 4 21\" />\n <motion.path className=\"clock\" d=\"M17.64 18.67 20 21\" />\n <motion.g className=\"plus\" style={{ transformOrigin: \"12px 13px\" }}>\n <path d=\"M12 10v6\" />\n <path d=\"M9 13h6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAlarmClockPlusIcon.displayName = \"AlarmClockPlusIcon\";\n\nexport default AlarmClockPlusIcon;\n",
"path": "icons/alarm-clock-plus-icon.tsx",
"content": "import { forwardRef, useImperativeHandle, useRef } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst AlarmClockPlusIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);\n\n const start = async () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animationControls.current.push(\n animate(\n \".clock\",\n {\n y: -1.5,\n x: [-1, 1, -1, 1, -1, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n animationControls.current.push(\n animate(\n \".bells\",\n {\n y: -2.5,\n x: [-2, 2, -2, 2, -2, 0],\n },\n {\n y: { duration: 0.2, type: \"spring\", stiffness: 200, damping: 25 },\n x: { duration: 0.3, repeat: Infinity, ease: \"linear\" },\n },\n ),\n );\n\n await animate(\n \".plus\",\n { scale: [1, 1.2, 1] },\n { duration: 0.4, ease: \"easeOut\" },\n );\n };\n\n const stop = () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n animate(\".clock\", { y: 0, x: 0 }, { duration: 0.2 });\n animate(\".bells\", { y: 0, x: 0 }, { duration: 0.2 });\n };\n\n useImperativeHandle(ref, () => {\n return {\n startAnimation: start,\n stopAnimation: stop,\n };\n });\n\n // 🖱 hover logic (only when NOT controlled)\n const handleHoverStart = () => {\n void start().catch(() => {});\n };\n\n const handleHoverEnd = () => {\n stop();\n };\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n style={{ overflow: \"visible\" }}\n >\n <motion.circle className=\"clock\" cx=\"12\" cy=\"13\" r=\"8\" />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"3.5px 4.5px\" }}\n d=\"M5 3 2 6\"\n />\n <motion.path\n className=\"bells\"\n style={{ transformOrigin: \"20.5px 4.5px\" }}\n d=\"m22 6-3-3\"\n />\n <motion.path className=\"clock\" d=\"M6.38 18.7 4 21\" />\n <motion.path className=\"clock\" d=\"M17.64 18.67 20 21\" />\n <motion.g className=\"plus\" style={{ transformOrigin: \"12px 13px\" }}>\n <path d=\"M12 10v6\" />\n <path d=\"M9 13h6\" />\n </motion.g>\n </motion.svg>\n );\n },\n);\n\nAlarmClockPlusIcon.displayName = \"AlarmClockPlusIcon\";\n\nexport default AlarmClockPlusIcon;\n",
🤖 Prompt for AI Agents
In @public/r/alarm-clock-plus-icon.json around lines 9 - 10, The hover handler
calls the async start() without handling rejections, which can cause unhandled
promise rejections; update the onHoverStart invocation (and the handleHoverStart
function) to handle errors by calling start with void and a catch (e.g., void
start().catch(() => {})) or wrap the call in an async try/catch, ensuring any
rejection from start() is swallowed; locate the start function and the
handleHoverStart/onHoverStart usage in AlarmClockPlusIcon to apply the change.

{
"path": "icons/washing-machine-icon.tsx",
"content": "import { forwardRef, useImperativeHandle, useCallback } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst WashingMachineIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = useCallback(async () => {\n // Continuously rotate the inner drum while hovering\n animate(\n \".drum-inner\",\n {\n rotate: [0, 360],\n },\n {\n duration: 2,\n repeat: Infinity,\n ease: \"linear\",\n },\n );\n }, [animate]);\n\n const stop = useCallback(async () => {\n // Stop the rotation\n animate(\n \".drum-inner\",\n {\n rotate: 0,\n },\n {\n duration: 0.3,\n ease: \"easeOut\",\n },\n );\n }, [animate]);\n\n useImperativeHandle(ref, () => ({\n startAnimation: start,\n stopAnimation: stop,\n }));\n\n return (\n <motion.svg\n ref={scope}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n onHoverStart={start}\n onHoverEnd={stop}\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n\n {/* Machine body */}\n <path d=\"M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z\" />\n\n {/* Drum outer */}\n <path d=\"M12 14m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0\" />\n\n {/* Control buttons */}\n <path d=\"M8 6h.01\" />\n <path d=\"M11 6h.01\" />\n <path d=\"M14 6h2\" />\n\n {/* Inner drum - rotating part */}\n <motion.path\n className=\"drum-inner\"\n d=\"M8 14c1.333 -.667 2.667 -.667 4 0c1.333 .667 2.667 .667 4 0\"\n style={{ transformOrigin: \"50% 58.33%\" }}\n />\n </motion.svg>\n );\n },\n);\n\nWashingMachineIcon.displayName = \"WashingMachineIcon\";\nexport default WashingMachineIcon;\n",
"content": "import { forwardRef, useImperativeHandle, useRef } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst WashingMachineIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 2, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n const animationControls = useRef<Array<ReturnType<typeof animate>>>([]);\n\n const start = async () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n // Continuously rotate the inner drum while hovering\n animationControls.current.push(\n animate(\n \".drum-inner\",\n {\n rotate: [0, 360],\n },\n {\n duration: 2,\n repeat: Infinity,\n ease: \"linear\",\n },\n ),\n );\n };\n\n const stop = () => {\n animationControls.current.forEach((control) => control.stop());\n animationControls.current = [];\n\n // Stop the rotation\n animate(\n \".drum-inner\",\n {\n rotate: 0,\n },\n {\n duration: 0.3,\n ease: \"easeOut\",\n },\n );\n };\n\n useImperativeHandle(ref, () => ({\n startAnimation: start,\n stopAnimation: stop,\n }));\n\n return (\n <motion.svg\n ref={scope}\n xmlns=\"http://www.w3.org/2000/svg\"\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`cursor-pointer ${className}`}\n onHoverStart={start}\n onHoverEnd={stop}\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n\n {/* Machine body */}\n <path d=\"M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z\" />\n\n {/* Drum outer */}\n <path d=\"M12 14m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0\" />\n\n {/* Control buttons */}\n <path d=\"M8 6h.01\" />\n <path d=\"M11 6h.01\" />\n <path d=\"M14 6h2\" />\n\n {/* Inner drum - rotating part */}\n <motion.path\n className=\"drum-inner\"\n d=\"M8 14c1.333 -.667 2.667 -.667 4 0c1.333 .667 2.667 .667 4 0\"\n style={{ transformOrigin: \"50% 58.33%\" }}\n />\n </motion.svg>\n );\n },\n);\n\nWashingMachineIcon.displayName = \"WashingMachineIcon\";\nexport default WashingMachineIcon;\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Track the reset animation in stop() for consistent cleanup.

In the embedded code's stop() function, the reset animation for .drum-inner (lines 37-46 of the embedded content) is not stored in animationControls. This means if stop() is called multiple times rapidly or start() is called during the reset, the reset animation cannot be stopped, potentially causing state conflicts.

🔧 Proposed fix

Store the reset animation in the controls array so it can be stopped if needed:

 const stop = () => {
   animationControls.current.forEach((control) => control.stop());
   animationControls.current = [];

   // Stop the rotation
-  animate(
+  animationControls.current.push(
+    animate(
       ".drum-inner",
       {
         rotate: 0,
       },
       {
         duration: 0.3,
         ease: "easeOut",
       },
-  );
+    ),
+  );
 };
🤖 Prompt for AI Agents
In @public/r/washing-machine-icon.json at line 10, The stop() function triggers
a reset animation for ".drum-inner" but does not track its returned control, so
it can't be stopped later; modify stop() to push the animate(...) return value
into animationControls.current (after first stopping/clearing existing controls
as done in start()) so the reset animation is tracked and can be cancelled if
stop() or start() is called again — update the animationControls.current usage
in stop() to mirror how start() stores its animate(...) control.

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