feat(icons): add animated brand icons for OpenAI, Gemini, Grok, Qwen,…#73
feat(icons): add animated brand icons for OpenAI, Gemini, Grok, Qwen,…#73Abhijit-Jha merged 3 commits intomasterfrom
Conversation
… Vercel, Cursor, Railway, AWS, React, React Native, Threads, Telegram, Zoom, Notion, Stripe, PayPal, and Windows
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRemoved 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
Sequence Diagram(s)(omitted) Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
Greptile OverviewGreptile SummaryThis 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 Key Changes
Issues Found
Notes
Confidence Score: 4/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
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
|
Additional Comments (3)
Prompt To Fix With AIThis 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.
Prompt To Fix With AIThis 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.
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 AIThis 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. |
There was a problem hiding this comment.
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
handleHoverStartandhandleHoverEndfunctions simply forward calls tostart()andstop(). 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 declaredasyncbut 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 theasync/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 usescursor-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: MissinguseCallbackimport.This component should use
useCallbackfor thestartandstopfunctions 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 inuseCallback.The
startandstopfunctions are recreated on every render. Other brand icons (e.g.,BrandQwenIcon,BrandNotionIcon) wrap these inuseCallbackfor 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 touseImperativeHandle.The
useImperativeHandlehook 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.
handleHoverStartandhandleHoverEndsimply callstartandstop. 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: MissinguseCallbackimport.Same pattern issue as
brand-paypal-icon.tsx. AdduseCallbackto 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 inuseCallback.The
startandstopfunctions should be wrapped inuseCallbackto 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 touseImperativeHandle.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
startandstopdirectly 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: Emptystopfunction creates inconsistent hover behavior.The embedded
BrandAwsIconcomponent has an emptystopfunction (const stop = () => {};), which meansonHoverEndandstopAnimationdo 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: Emptystopfunction - animation cannot be interrupted.The
stopfunction is a no-op, meaning rapid hover interactions can stack multiple animation sequences. Consider resetting the animated elements to their initial state, similar toBrandOpenaiIcon: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 wrappingstartwithuseCallbackfor consistency.
BrandVercelIconwraps its animation functions withuseCallback, butBrandAwsIcondoes 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: Missingselect-noneclass.Other brand icons include
select-nonein 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 withuseCallbackfor consistency.For consistency with
BrandVercelIcon, consider wrappingstartandstopwithuseCallback. This is a minor optimization since the functions are recreated on each render.icons/brand-windows-icon.tsx (2)
131-136: Missing dependency array inuseImperativeHandle.Unlike other icons in this PR (e.g.,
BrandQwenIcon,BrandGeminiIcon), this component omits the dependency array foruseImperativeHandle. 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
startandstopinuseCallbackto ensure stable references, similar to the pattern used inBrandQwenIconandBrandGeminiIcon.
138-144: Redundant hover handler wrappers.
handleHoverStartandhandleHoverEndsimply delegate tostartandstop. You can passstartandstopdirectly toonHoverStartandonHoverEnd.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 inuseImperativeHandle.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:useCallbackis imported but not used;start/stopshould be memoized.The component imports
useCallbackbut doesn't use it. Thestartandstopasync functions are recreated on every render, which defeats the purpose of the[start, stop]dependency array inuseImperativeHandle.Proposed fix
Either remove the unused
useCallbackimport if memoization is not intended:-import { forwardRef, useImperativeHandle, useCallback } from "react"; +import { forwardRef, useImperativeHandle } from "react";Or wrap the functions with
useCallbackfor 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 memoizingstart/stop(see above), pass them as deps touseImperativeHandle.icons/brand-railway-icon.tsx (1)
1-3: Wrapstart/stopinuseCallback(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: ConsiderAnimatedIconHandlereturn types asvoid | Promise<void>to match async icons.
Line 15 definesstartAnimation: () => void/stopAnimation: () => void, but many icons in this PR useasyncstart/stop (e.g., embedded at Line 10). Aligning the type makes it clearer for consumers (and allowsawait ref.current?.startAnimation()patterns).public/r/brand-windows-icon.json (1)
1-20: Inconsistent hook patterns compared to BrandCursorIcon.The embedded
BrandWindowsIconcomponent doesn't wrapstartandstopfunctions inuseCallbackand lacks a dependency array inuseImperativeHandle. In contrast,BrandCursorIcon(inpublic/r/brand-cursor-icon.json) properly usesuseCallbackwith dependencies and includes[start, stop]inuseImperativeHandle.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
📒 Files selected for processing (39)
app/layout.tsxicons/brand-aws-icon.tsxicons/brand-cursor-icon.tsxicons/brand-gemini-icon.tsxicons/brand-grok-icon.tsxicons/brand-notion-icon.tsxicons/brand-openai-icon.tsxicons/brand-paypal-icon.tsxicons/brand-qwen-icon.tsxicons/brand-railway-icon.tsxicons/brand-react-icon.tsxicons/brand-react-native-icon.tsxicons/brand-stripe-icon.tsxicons/brand-telegram-icon.tsxicons/brand-threads-icon.tsxicons/brand-vercel-icon.tsxicons/brand-windows-icon.tsxicons/brand-zoom-icon.tsxicons/index.tspackage.jsonpublic/r/brand-aws-icon.jsonpublic/r/brand-cursor-icon.jsonpublic/r/brand-gemini-icon.jsonpublic/r/brand-grok-icon.jsonpublic/r/brand-notion-icon.jsonpublic/r/brand-openai-icon.jsonpublic/r/brand-paypal-icon.jsonpublic/r/brand-qwen-icon.jsonpublic/r/brand-railway-icon.jsonpublic/r/brand-react-icon.jsonpublic/r/brand-react-native-icon.jsonpublic/r/brand-stripe-icon.jsonpublic/r/brand-telegram-icon.jsonpublic/r/brand-threads-icon.jsonpublic/r/brand-vercel-icon.jsonpublic/r/brand-windows-icon.jsonpublic/r/brand-zoom-icon.jsonpublic/r/registry.jsonregistry.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
BrandGrokIconaligns 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:buildscript). After addressing the animation selector issue inicons/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:buildicons/brand-qwen-icon.tsx (1)
5-62: LGTM!The rename to
BrandQwenIconis consistent with the registry-wide branding updates. The animation logic withuseCallbackand 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
BrandNotionIconaligns with the branding convention. The animation approach using class selectors (.notion-shell,.notion-letter) for targeting child elements is appropriate, and theuseCallbackdependencies correctly include onlyanimate.icons/brand-gemini-icon.tsx (1)
5-73: LGTM!The rename to
BrandGeminiIconis consistent with the branding convention. The implementation correctly usesuseCallbackwith proper dependencies, and includes good accessibility attributes (role="img",aria-label,title). ThetransformBoxandtransformOriginstyles 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(addinguseCallback, dependency array foruseImperativeHandle, removing wrapper functions).public/r/brand-openai-icon.json (1)
1-20: LGTM!The registry structure is correct. The embedded
BrandOpenaiIconcomponent properly implements bothstartandstopanimations with staggered delays for a polished visual effect.icons/brand-vercel-icon.tsx (1)
20-26: Verify intended rotation behavior onstop.The
stopfunction rotates torotateY: -180rather than resetting to0. 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
stopfunction 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.tsxapplies to the embedded content here.public/r/brand-qwen-icon.json (1)
1-20: LGTM!The component follows proper React patterns with
useCallbackfor memoizing animation functions and correct dependency arrays foruseImperativeHandle. 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 touseImperativeHandlefor consistency with icons likeBrandQwenIconandBrandGeminiIcon.public/r/brand-gemini-icon.json (1)
1-20: LGTM!Excellent implementation with proper
useCallbackmemoization, 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-embeddedBrandReactIconbehavior in sync withicons/brand-react-icon.tsxfixes.
Once you address the animation cancellation / repeat behavior inicons/brand-react-icon.tsx, ensure the embeddedcontenthere is updated accordingly (to prevent the registry version drifting).public/r/brand-notion-icon.json (1)
9-12: VerifyrotateYon SVG elements behaves as intended across browsers.
The embeddedstart()animatesrotateYon.notion-shell; depending onmotion/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
BrandCursorIconimplementation properly usesuseCallbackwith appropriate dependencies and includes a dependency array inuseImperativeHandle. 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.jsonfiles appear to be auto-generated outputs that should remain in sync.
| 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 }, | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "brand-react-icon.tsx" -type fRepository: itshover/itshover
Length of output: 89
🏁 Script executed:
cat -n ./icons/brand-react-icon.tsxRepository: 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]);| 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", | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| "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" | ||
| }, |
There was a problem hiding this comment.
🧩 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 5Repository: 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 -20Repository: 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.
| "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", |
There was a problem hiding this comment.
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.
| "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.
There was a problem hiding this comment.
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. Ifstart()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: UpdateAnimatedIconHandletype to match implementation.The type definition declares
startAnimation: () => voidbut the implementation isasync () => {...}, which returnsPromise<void>. Updateicons/types.tsline 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.plusanimation too (rapid hover can stack multiple scale animations).Right now only
.ambulanceis stored/stopped; the.plusanimation 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 touseImperativeHandle.The
useImperativeHandlehook is missing its dependency array (third argument). Without it, the imperative handle object is recreated on every render, which is inefficient. SinceanimatefromuseAnimate()andanimationControls(a ref) have stable references, an empty dependency array is appropriate.♻️ Suggested fix
In the embedded component code, update the
useImperativeHandlecall:- useImperativeHandle(ref, () => ({ - startAnimation: start, - stopAnimation: stop, - })); + useImperativeHandle(ref, () => ({ + startAnimation: start, + stopAnimation: stop, + }), []);
10-10: Remove unnecessaryasynckeyword fromstartfunction.The
startfunction is declared asasyncbut doesn't useawait. Theanimate()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. Ifstart()is called immediately afterstop()(within 0.3s), both animations will briefly compete for the samex/yproperties, 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:.plusanimation handle is not stored.The AI summary states that the plus animation handle is "stored as well," but the actual code only stores the
.ambulanceanimation handle:animationControls.current.push( animate(".ambulance", ...), // stored ); animate(".plus", ...); // NOT storedThis is acceptable since the
.plusanimation is finite (norepeat: Infinity), but the summary is misleading.Additionally, the
startfunction is markedasyncbut contains noawaitexpressions—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 unnecessaryasynckeyword.The
startfunction is markedasyncbut contains noawaitexpressions. Theanimatecalls 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 touseImperativeHandle.Without a dependency array, the handle object is recreated on every render. Since
startandstopare stable (they only referenceanimateandanimationControls.currentwhich don't change identity), an empty dependency array would prevent unnecessary handle updates.♻️ Suggested fix
useImperativeHandle(ref, () => ({ startAnimation: start, stopAnimation: stop, - })); + }), []);Alternatively, wrap
startandstopwithuseCallbackif 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.currentbut 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 auseEffectcleanup function to prevent potential memory leaks or errors if the component unmounts mid-animation.♻️ Suggested cleanup implementation
Add this cleanup effect after the
useImperativeHandlecall 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.currentto 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,
BrandVercelIcondoesn't use theanimationControlsref pattern. Rapid hover in/out could trigger overlapping animations sincestart/stopdon't cancel in-progress animations.Additionally,
rotateYwithout a CSSperspectiveproperty 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 addingtransformOriginfor the center-dot element.The
vinyl-discpath has explicittransformOrigin: "50% 50%", but thecenter-dotpath lacks it. While scale animations typically default to center, adding explicittransformOriginwould 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
handleHoverStartandhandleHoverEndfunctions simply delegate tostartandstopwithout any additional logic. You can passstartandstopdirectly 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
namefield 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 ifstop()is called before they complete.Additionally,
start()isasyncbutAnimatedIconHandledeclaresstartAnimation: () => 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
AnimatedIconHandleto 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 bystart(), 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: 0ensures instant reset, eliminating potential overlap with subsequentstart()calls.icons/docker-icon.tsx (2)
13-36:startisasyncbut neverawaits; also the.dotanimation isn’t tracked/stoppable.
Make it sync unless you intentionally need aPromise, and consider tracking.dottoo 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: UseAnimationPlaybackControlsfrom "motion/react" instead ofReturnType<typeof animate>for better type clarity and safety.The animate function in motion v12.23.26 returns
AnimationPlaybackControls(orAnimationPlaybackControlsWithThen), which is exported from "motion/react". Using the concrete type directly—rather than inferring it viaReturnType—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/onHoverEndalways fire; if a parent is also drivingstartAnimation/stopAnimation, the two can fight. Consider a prop likehoverEnabled?: boolean(defaulttrue) 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
📒 Files selected for processing (35)
app/layout.tsxicons/accessibility-icon.tsxicons/alarm-clock-plus-icon.tsxicons/ambulance-icon.tsxicons/brand-railway-icon.tsxicons/brand-react-icon.tsxicons/brand-react-native-icon.tsxicons/brand-stripe-icon.tsxicons/brand-threads-icon.tsxicons/brand-vercel-icon.tsxicons/docker-icon.tsxicons/globe-icon.tsxicons/mouse-pointer-2-icon.tsxicons/qrcode-icon.tsxicons/spotify-icon.tsxicons/vinyl-icon.tsxicons/washing-machine-icon.tsxlib/icon-names.tslib/icons.tspublic/r/accessibility-icon.jsonpublic/r/alarm-clock-plus-icon.jsonpublic/r/ambulance-icon.jsonpublic/r/brand-railway-icon.jsonpublic/r/brand-react-icon.jsonpublic/r/brand-react-native-icon.jsonpublic/r/brand-stripe-icon.jsonpublic/r/brand-threads-icon.jsonpublic/r/brand-vercel-icon.jsonpublic/r/docker-icon.jsonpublic/r/globe-icon.jsonpublic/r/mouse-pointer-2-icon.jsonpublic/r/qrcode-icon.jsonpublic/r/spotify-icon.jsonpublic/r/vinyl-icon.jsonpublic/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
useRefandanimationControlsref 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
forEachpattern 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
AnimatedIconHandleinterface 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
animationControlsref 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:
asynckeyword on line 13 is unnecessary since there's noawait, but it doesn't cause any issues.
40-67: Imperative handle and rendering look good.The
useImperativeHandlecorrectly exposesstartAnimationandstopAnimationas perAnimatedIconHandle, 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
useRefto store animation controls with correct type inference viaReturnType<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
animationControlsref 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
animationControlspattern. 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-scananimation is tracked inanimationControlssince 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 stateicons/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
useImperativeHandleto expose the animation API consistently with other icons- The
stopfunction correctly resets rotation state after stopping tracked animationsicons/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
animationControlsref 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
animationControlsref 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
RailwayIcontoBrandRailwayIconaligns with the broader PR objective of standardizing brand icon naming. The removal ofuseCallbackin 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 uniqueclipPathIDs is a solid pattern that prevents conflicts when multipleBrandStripeIconinstances 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
styleprops 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
motiondependency, 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
useRefand theanimationControlsref 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 inanimationControlsand 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. Theanimate()function from motion v12.23.26 always returnsAnimationPlaybackControls, which includes astop(): voidmethod. The type is guaranteed by the library, soReturnType<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.ambulanceloops.
| 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, | ||
| })); |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| })); |
There was a problem hiding this comment.
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.
| 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.
| 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], | ||
| }, | ||
| ); | ||
| }; |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
| "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", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find package.json to check the motion/react version
find . -name "package.json" -type f | head -5Repository: 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 -80Repository: 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 motionRepository: itshover/itshover
Length of output: 43
🏁 Script executed:
cat package.json | head -50Repository: itshover/itshover
Length of output: 1545
🏁 Script executed:
# Search for useAnimate and animate patterns in the codebase
rg "useAnimate" -A 5 -B 1 | head -100Repository: itshover/itshover
Length of output: 45182
🏁 Script executed:
# Search for animate\( calls to see usage patterns
rg "animate\(" -A 3 | head -100Repository: 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 -20Repository: 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 -20Repository: 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 -5Repository: 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 -40Repository: 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 -40Repository: 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:
- Stacked animations on repeated calls: Calling
start()multiple times (e.g., rapid hover or programmatic calls) launches new.plusscale animations without stopping the previous ones, causing overlapping visual effects. - Incomplete reset:
stop()doesn't reset.plusscale 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.
🧩 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:
- 1: https://motion.dev/docs/improvements-to-the-web-animations-api-dx?utm_source=openai
- 2: https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished?utm_source=openai
🌐 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
-
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]
- onMouseEnter={() => handleHoverStart()} with
-
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]
-
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]
-
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.
-
Guard against component unmounts:
- Track mounted state or cancel requests (AbortController) before calling setState after an awaited result to avoid updates after unmount.
-
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]
-
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 -50Repository: 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.
| "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", |
There was a problem hiding this comment.
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.
feat(icons): add animated brand icons for major tech platforms
Added the following brand icons:
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.