Icons/notion gwen grok golang cursor railway gemini#65
Conversation
|
@itstheanurag is attempting to deploy a commit to the itshover's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds seven new forwardRef animated React icon components (Cursor IDE, Gemini, Golang, Grok, Notion, Qwen, Railway) with imperative handles and hover animations, a shared Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @icons/notion-icon.tsx:
- Around line 19-22: The useImperativeHandle call is missing its dependency
array which can lead to stale closures; update the useImperativeHandle(ref, ()
=> ({ startAnimation: start, stopAnimation: stop })) invocation to include the
proper dependencies (e.g., [start, stop]) so the imperative handle updates
whenever the start or stop functions change.
In @icons/qwen-icon.tsx:
- Around line 37-40: The useImperativeHandle call exposing
startAnimation/stopAnimation is missing a dependency array, which can cause
stale closures; update the useImperativeHandle(ref, () => ({ startAnimation:
start, stopAnimation: stop })) invocation to include a dependency array listing
start and stop (and ref if applicable) so the imperative handle is recreated
when those functions change.
In @icons/railway-icon.tsx:
- Around line 24-27: The useImperativeHandle call in railway-icon.tsx should
include a dependency array to avoid stale closures; update the
useImperativeHandle(ref, () => ({ startAnimation: start, stopAnimation: stop,
})) invocation to pass [start, stop] (or the exact start/stop refs used) as the
third argument so the imperative handle updates when start or stop change.
In @public/r/cursor-ide-icon.json:
- Around line 1-20: The CursorIdeIcon component accepts a color prop but the SVG
hardcodes fill="currentColor"; update the JSX in the CursorIdeIcon return so the
motion.svg uses the passed color (fill={color}) instead of "currentColor" so the
AnimatedIconProps.color is applied; locate the component defined via forwardRef
(CursorIdeIcon) and replace the hardcoded fill with the color prop.
In @public/r/railway-icon.json:
- Around line 1-20: The two motion.path elements inside the RailwayIcon
component have their className values swapped relative to their comments: the
path commented "Outer shell" currently uses className="railway-inner" and the
path commented "Inner moving track" uses className="railway-outer"; update those
className props so the outer shell uses "railway-outer" and the inner track uses
"railway-inner" (and adjust the inline comments if needed) to make the names and
comments consistent for future animations.
🧹 Nitpick comments (6)
lib/icons.ts (1)
666-693: Consider maintaining alphabetical order for consistency.The new icon entries are correctly structured, but they're appended at the end rather than inserted alphabetically like the existing entries. This is a minor inconsistency that could affect maintainability as the list grows.
♻️ Suggested alphabetical placement
The icons could be inserted at their alphabetically correct positions:
cursor ide icon→ after "currency-rupee-icon"gemini icon→ after "gauge-icon"golang icon→ after "gmail-icon"grok icon→ after "globe-icon"notion icon→ after "nodejs-icon"qwen icon→ after "qrcode-icon"railway icon→ after "rainbow-icon"icons/railway-icon.tsx (1)
45-55: Comment/className mismatch may cause confusion.The comments describe "Outer shell" and "Inner moving track" but the classNames are reversed (
railway-innerfor outer,railway-outerfor inner). While this doesn't affect functionality since these classes aren't used in the animation selectors, it could confuse future maintainers.icons/gemini-icon.tsx (1)
50-79: Rotation/scale may need explicit SVG transform origin to look right.
For consistent “spin around center” behavior across browsers, consider addingtransformBox/transformOrigin(or Motion equivalents) on the<motion.svg>.Possible adjustment
<motion.svg ref={scope} onHoverStart={start} onHoverEnd={stop} fill={color} viewBox="0 0 24 24" height={size} width={size} - style={{ flex: "none", lineHeight: 1 }} + style={{ + flex: "none", + lineHeight: 1, + transformBox: "fill-box", + transformOrigin: "center", + }} xmlns="http://www.w3.org/2000/svg" className={className} >icons/golang-icon.tsx (1)
34-62: Consider adding a<title>(or mark as decorative) for consistency.
Other new icons include<title>…</title>; this one doesn’t. Either add a title or explicitly setaria-hiddenif icons are decorative in your UI.public/r/gemini-icon.json (1)
1-20: If you change GeminiIcon animation scoping, mirror it here (or generate this JSON).
Right now this registry item will keep any selector-scoping issues even if the TSX file is later improved.public/r/qwen-icon.json (1)
1-20: QwenIcon registry entry inherits the same selector-scoping/transform-origin concerns.
If you decide to harden Gemini’s implementation (scoped selectors + explicit origin), consider applying the same pattern here for consistency.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
icons/cursor-ide-icon.tsxicons/gemini-icon.tsxicons/golang-icon.tsxicons/grok-icon.tsxicons/index.tsicons/notion-icon.tsxicons/qwen-icon.tsxicons/railway-icon.tsxlib/icons.tspublic/r/cursor-ide-icon.jsonpublic/r/gemini-icon.jsonpublic/r/golang-icon.jsonpublic/r/grok-icon.jsonpublic/r/hotel-icon.jsonpublic/r/notion-icon.jsonpublic/r/qwen-icon.jsonpublic/r/railway-icon.jsonpublic/r/registry.jsonpublic/r/travel-bag.jsonregistry.json
🔇 Additional comments (14)
public/r/hotel-icon.json (1)
10-10: LGTM!The formatting normalization of the content field is consistent with the codebase standards. No functional changes detected.
public/r/travel-bag.json (1)
10-10: LGTM!Formatting alignment change with no functional impact. The component structure remains consistent with the icon library conventions.
icons/notion-icon.tsx (1)
1-57: LGTM!Clean implementation following the established icon pattern. The animation effect (scale on letter, opacity on shell) provides a subtle and appropriate hover interaction.
icons/qwen-icon.tsx (1)
9-35: Consider animation timing for UX consistency.The 1-second animation duration is notably longer than other icons in this PR (0.2-0.4s). While this may be intentional for the collapsing effect, it could feel inconsistent with the snappier animations of sibling icons. If users hover briefly, they may not see the full animation complete.
public/r/golang-icon.json (1)
1-20: Registry entry looks good.The registry structure is correct with appropriate dependencies on
motionand includes both the component and types files.Note: The embedded component code has the same
useImperativeHandlemissing dependency array issue identified in other icon files. Ensure the source fileicons/golang-icon.tsxis updated and this registry JSON is regenerated.icons/gemini-icon.tsx (1)
9-43: No action needed. Theanimate(".gemini-*")selectors are automatically scoped by motion/react to descendants ofscope.current(the motion.svg element), not the document. Multiple GeminiIcon instances will not interfere with each other.Likely an incorrect or invalid review comment.
icons/golang-icon.tsx (1)
12-27: This concern is incorrect. The code uses Framer Motion'suseAnimate()hook correctly. Each component instance receives its own[scope, animate]pair; when a component animates".speed-line", it only targets elements within that specific SVG instance via theref={scope}binding, not globally across the page. There is no selector-scoping risk or multi-instance bleed.Likely an incorrect or invalid review comment.
icons/grok-icon.tsx (1)
1-85: LGTM!The GrokIcon component is well-implemented following the established pattern for animated icons. The spring animations for scale and timed animations for opacity transitions create a smooth hover effect. The imperative handle correctly exposes
startAnimationandstopAnimationmethods.registry.json (1)
1163-1179: Auto-generated registry entries look correct.The seven new icon entries follow the established structure with proper dependencies on
motionand correct file references. Since this is an auto-generated file, ensurenpm run registry:buildwas run to generate these entries rather than manual edits.Also applies to: 1554-1570, 1673-1706, 2166-2182, 2387-2403, 2421-2437
public/r/notion-icon.json (1)
1-20: LGTM!The registry item for NotionIcon is properly structured with the correct schema, dependencies, and embedded source files. The NotionIcon implementation follows the established animated icon pattern.
icons/index.ts (2)
191-197: LGTM!The imports for the seven new icon components are correctly added.
1386-1428: LGTM!The ICON_LIST entries for all seven new icons are well-structured with appropriate keywords for search functionality. The entries follow the established pattern and will be correctly sorted alphabetically by the
.sort()call at the end of the array.public/r/grok-icon.json (1)
1-20: LGTM!The registry item for GrokIcon is properly structured and the embedded source matches the standalone
icons/grok-icon.tsxfile.public/r/registry.json (1)
1163-1179: Auto-generated registry entries are consistent.The seven new icon entries in
public/r/registry.jsonmatch those in the rootregistry.json. Both files are auto-generated and properly synchronized.Also applies to: 1554-1570, 1673-1706, 2166-2182, 2387-2403, 2421-2437
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "railway-icon", | ||
| "dependencies": ["motion"], | ||
| "devDependencies": [], | ||
| "registryDependencies": [], | ||
| "files": [ | ||
| { | ||
| "path": "icons/railway-icon.tsx", | ||
| "content": "import { forwardRef, useImperativeHandle, useCallback } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst RailwayIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n ({ size = 24, color = \"currentColor\", className = \"\" }, ref) => {\n const [scope, animate] = useAnimate();\n\n const start = useCallback(() => {\n animate(\n scope.current,\n {\n x: [0, -2, 2, -2, 0],\n y: [0, 1, -1, 1, 0],\n },\n { duration: 0.25, repeat: 1 },\n );\n }, [animate, scope]);\n\n const stop = useCallback(() => {\n animate(scope.current, { x: 0, y: 0 }, { duration: 0.15 });\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 fillRule=\"evenodd\"\n height={size}\n width={size}\n style={{ flex: \"none\", lineHeight: 1 }}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n >\n <title>Railway</title>\n\n {/* Outer shell */}\n <motion.path\n className=\"railway-inner\"\n d=\"M.111 10.27c-.057.4-.094.804-.111 1.208h18.23a2.182 2.182 0 00-.236-.346c-3.116-4.026-4.793-3.677-7.19-3.78-.8-.032-1.343-.046-4.525-.046-1.704 0-3.555.005-5.359.01-.233.63-.458 1.24-.568 1.737h9.342v1.217H.11zm18.262 2.426H.01c.02.325.05.645.094.961h16.954c.756 0 1.179-.429 1.316-.96zm-17.318 4.28S3.865 23.878 11.987 24c4.854 0 9.025-2.883 10.92-7.024H1.055z\"\n />\n\n {/* Inner moving track */}\n <motion.path\n className=\"railway-outer\"\n d=\"M11.987 0C7.5 0 3.593 2.465 1.531 6.108c1.611-.003 4.75-.005 4.75-.005h.001v-.001c3.71 0 3.848.016 4.573.046l.45.017c1.562.052 3.484.22 4.996 1.364.82.62 2.005 1.99 2.712 2.965.653.902.84 1.94.396 2.934-.408.913-1.287 1.458-2.352 1.458H.39s.1.42.248.885h22.75a11.96 11.96 0 00.61-3.766C24 5.375 18.623 0 11.988 0z\"\n />\n </motion.svg>\n );\n },\n);\n\nRailwayIcon.displayName = \"RailwayIcon\";\nexport default RailwayIcon;\n", | ||
| "type": "registry:ui" | ||
| }, | ||
| { | ||
| "path": "icons/types.ts", | ||
| "content": "export interface AnimatedIconProps {\n /** Icon size in pixels or CSS string */\n size?: number | string;\n /** Icon color (defaults to currentColor) */\n color?: string;\n /** SVG stroke width */\n strokeWidth?: number;\n /** Additional CSS classes */\n className?: string;\n}\n\nexport interface AnimatedIconHandle {\n startAnimation: () => void;\n stopAnimation: () => void;\n}\n", | ||
| "type": "registry:ui" | ||
| } | ||
| ], | ||
| "type": "registry:ui" | ||
| } |
There was a problem hiding this comment.
Minor: path classNames/comments are swapped/misleading in embedded TSX.
Non-blocking, but it makes future edits harder (especially if you later animate inner/outer paths).
🤖 Prompt for AI Agents
In @public/r/railway-icon.json around lines 1 - 20, The two motion.path elements
inside the RailwayIcon component have their className values swapped relative to
their comments: the path commented "Outer shell" currently uses
className="railway-inner" and the path commented "Inner moving track" uses
className="railway-outer"; update those className props so the outer shell uses
"railway-outer" and the inner track uses "railway-inner" (and adjust the inline
comments if needed) to make the names and comments consistent for future
animations.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @icons/qwen-icon.tsx:
- Around line 26-34: The stop callback has mismatched durations: transform
animate on scope.current uses duration 1s while opacity toggles for
".qwen-shape" and ".qwen-core" use 0.2s, causing an 800ms visual gap; update the
durations so transform and opacity finish together (e.g., change the
scope.current animate duration from 1 to 0.2 or increase the opacity animates to
1) in the stop function that references scope.current, ".qwen-shape" and
".qwen-core" so the scale/rotate and opacity transitions are synchronized.
In @public/r/qwen-icon.json:
- Around line 9-10: The stop callback's opacity animations use duration 0.2
while the rotation/scale use duration 1, causing inconsistent reverse timing;
update the stop function (referencing stop, animate, scope.current,
".qwen-shape", and ".qwen-core") to use the same duration and easing as start
(e.g., duration: 1 and ease: "easeInOut" for both opacity animations) so
rotation/scale and opacity reverse together, or intentionally stagger them if
you want a different effect.
🧹 Nitpick comments (11)
icons/railway-icon.tsx (1)
9-22: Consider adding defensive null checks for scope.current.If
startAnimationorstopAnimationare called imperatively via the ref handle before the component mounts,scope.currentwill benull, potentially causing a runtime error. Consider adding a guard:🛡️ Defensive fix
const start = useCallback(() => { + if (!scope.current) return; animate( scope.current, { x: [0, -2, 2, -2, 0], y: [0, 1, -1, 1, 0], }, { duration: 0.25, repeat: 1 }, ); }, [animate, scope]); const stop = useCallback(() => { + if (!scope.current) return; animate(scope.current, { x: 0, y: 0 }, { duration: 0.15 }); }, [animate, scope]);icons/cursor-ide-icon.tsx (2)
12-25: Consider adding defensive null checks for scope.current.Same consideration as RailwayIcon: if animation methods are called imperatively before mount,
scope.currentwill benull.🛡️ Defensive fix
const start = useCallback(() => { + if (!scope.current) return; animate( scope.current, { x: [0, -1, 1, -1, 0], y: [0, 1, -1, 1, 0], }, { duration: 0.25, repeat: 1 }, ); }, [animate, scope]); const stop = useCallback(() => { + if (!scope.current) return; animate(scope.current, { x: 0, y: 0 }, { duration: 0.15 }); }, [animate, scope]);
49-50: Verify whether strokeWidth is actually used.The
strokeandstrokeWidthattributes are applied to the SVG, but the path appears to use fill-based rendering. If the icon doesn't require stroke styling, these attributes (and thestrokeWidthprop) might be unnecessary.If stroke is not needed, consider this cleanup:
♻️ Optional cleanup if stroke is unused
Remove from props:
( - { size = 24, color = "currentColor", strokeWidth = 1, className = "" }, + { size = 24, color = "currentColor", className = "" }, ref, ) => {Remove from SVG attributes:
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className} - stroke={color} - strokeWidth={strokeWidth} >And remove strokeWidth from AnimatedIconProps in types.ts if no other icons use it.
public/r/notion-icon.json (1)
1-20: LGTM! Registry correctly configured, and the embedded component shows a nice pattern.The registry metadata is properly structured. Notably, the NotionIcon implementation (visible in the embedded content) uses class selectors (
.notion-letter,.notion-shell) to animate specific SVG paths independently, which is a cleaner approach than animating the entire scope when you need fine-grained control over multiple elements.Consider applying the class-selector animation pattern from NotionIcon to other icons if they need to animate multiple elements with different timing or properties. This approach:
- Avoids needing to pass
scope.currentexplicitly- Makes it clearer which elements are being animated
- Allows independent animation of different parts
Example from NotionIcon:
const start = useCallback(() => { animate(".notion-letter", { scale: 1.1 }, { duration: 0.2 }); animate(".notion-shell", { opacity: 0.7 }, { duration: 0.2 }); }, [animate]);icons/gemini-icon.tsx (2)
6-6: Inconsistent prop destructuring with AnimatedIconProps interface.The component doesn't destructure
strokeWidthfrom props, butAnimatedIconPropsincludes it. Since GeminiIcon usesfillrather thanstroke,strokeWidthisn't applicable here. Consider either:
- Destructuring
strokeWidth(even if unused) for API consistency, or- Adding a comment explaining why it's omitted for fill-based icons.
♻️ Option 1: Accept strokeWidth for consistency
- ({ size = 24, color = "currentColor", className = "" }, ref) => { + ({ size = 24, color = "currentColor", strokeWidth, className = "" }, ref) => {
54-66: Consider adding cursor-pointer class for consistency.The GolangIcon component includes
cursor-pointerin its className (seegolang-icon.tsxline 52), but GeminiIcon doesn't. Since the icon responds to hover interactions, adding a pointer cursor would provide consistent visual feedback across all animated icons.♻️ Add cursor-pointer class
- className={className} + className={`cursor-pointer ${className}`}icons/qwen-icon.tsx (1)
45-60: Add cursor-pointer for interactive feedback.Similar to GeminiIcon, QwenIcon lacks the
cursor-pointerclass present in GolangIcon. For consistency and better UX on hover-interactive icons, consider adding it.♻️ Add cursor-pointer class
- className={className} + className={`cursor-pointer ${className}`}icons/grok-icon.tsx (4)
54-66: Make the scale animation deterministic by pinning SVG transform origin.Depending on browser/SVG defaults, scaling the
<svg>can appear to shift position. SettingtransformOrigin(and, for SVG,transformBox) makes the “collapse into core” effect consistent.Proposed patch
<motion.svg ref={scope} onHoverStart={start} onHoverEnd={stop} fill={color} fillRule="evenodd" viewBox="0 0 24 24" height={size} width={size} - style={{ flex: "none", lineHeight: 1 }} + style={{ + flex: "none", + lineHeight: 1, + transformOrigin: "50% 50%", + transformBox: "fill-box", + }} xmlns="http://www.w3.org/2000/svg" className={className} >
53-68: Add a11y hooks (aria-label / aria-hidden) instead of relying on<title>alone.As-is, consumers can’t easily control whether this SVG is decorative vs. meaningful (and
<title>behavior is inconsistent across AT/browsers). Consider either passing through SVG props or adding anariaLabelprop and defaulting toaria-hiddenwhen absent.
58-82: Consider movingfillRule="evenodd"to the<path>for clarity. (developer.mozilla.org)
fill-ruleis inherited as a CSS property, but the attribute is typically applied on the shape element; putting it on the<motion.path>makes the intent unambiguous (especially if more shapes get added later).Proposed patch
- fillRule="evenodd" + fillRule="evenodd" viewBox="0 0 24 24" @@ <motion.path className="grok-shape" + fillRule="evenodd" 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.292M7.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" />
9-42: Avoid stacking overlapping animations on rapid hover in/out by storing and stopping previous animation controls.Each
start()/stop()call fires new animations without stopping prior ones. Rapid hover jitter can accumulate on the spring scale and opacity tweens. Motion'sanimate()returnsAnimationControlswith a.stop()method—store the main scale animation control in a ref and call.stop()before triggering a new animation sequence.Proposed patch (track controls and stop before starting new)
-import { forwardRef, useImperativeHandle, useCallback } from "react"; +import { forwardRef, useImperativeHandle, useCallback, useRef } from "react"; import type { AnimatedIconHandle, AnimatedIconProps } from "./types"; import { motion, useAnimate } from "motion/react"; const GrokIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>( ({ size = 24, color = "currentColor", className = "" }, ref) => { const [scope, animate] = useAnimate(); + const controlsRef = useRef<{ stop: () => void } | null>(null); const start = useCallback(() => { + controlsRef.current?.stop(); // Compress the whole icon into the core - animate( + const controls = animate( scope.current, { scale: 0.45 }, { type: "spring", stiffness: 300, damping: 25 }, ); + controlsRef.current = controls; // Fade out Grok shape animate( ".grok-shape", { opacity: 0 }, { duration: 0.25, ease: "easeOut" }, ); // Fade in core animate( ".grok-core", { opacity: 1 }, { duration: 0.25, ease: "easeOut" }, ); }, [animate, scope]); const stop = useCallback(() => { + controlsRef.current?.stop(); // Spring back to full size - animate( + const controls = animate( scope.current, { scale: 1 }, { type: "spring", stiffness: 260, damping: 18 }, ); + controlsRef.current = controls; animate(".grok-shape", { opacity: 1 }, { duration: 0.2 }); animate(".grok-core", { opacity: 0 }, { duration: 0.2 }); }, [animate, scope]);
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
icons/cursor-ide-icon.tsxicons/gemini-icon.tsxicons/golang-icon.tsxicons/grok-icon.tsxicons/notion-icon.tsxicons/qwen-icon.tsxicons/railway-icon.tsxpublic/r/cursor-ide-icon.jsonpublic/r/gemini-icon.jsonpublic/r/golang-icon.jsonpublic/r/grok-icon.jsonpublic/r/notion-icon.jsonpublic/r/qwen-icon.jsonpublic/r/railway-icon.json
🚧 Files skipped from review as they are similar to previous changes (2)
- icons/notion-icon.tsx
- public/r/grok-icon.json
🔇 Additional comments (13)
icons/railway-icon.tsx (2)
24-61: LGTM! Clean integration of motion animations with imperative handle.The component correctly wires up the imperative API, hover events, and animation scope. The SVG structure is well-organized with clear comments distinguishing the outer shell from the inner track.
65-66: LGTM!Display name is set correctly for debugging, and the component is properly exported.
public/r/railway-icon.json (1)
1-20: LGTM! Registry metadata is correctly structured.The registry file properly declares the motion dependency, includes both the component and shared types file, and follows the expected schema structure.
public/r/cursor-ide-icon.json (1)
1-20: LGTM! Registry metadata follows the correct pattern.Consistent with other icon registry files, properly declaring motion dependency and including necessary files.
icons/cursor-ide-icon.tsx (1)
27-60: LGTM! Imperative API and component structure are well implemented.The component follows the established pattern with correct forwardRef usage, imperative handle exposure, and hover event bindings.
icons/gemini-icon.tsx (1)
32-43: Verify intentional timing mismatch in stop animation.The
stopcallback resets transforms over 0.5s (lines 34-38) but fades elements in 0.2s (lines 41-42). This causes the gemini-shape to fully reappear 300ms before the icon finishes scaling back to normal size, which might create a visual pop-in effect.If this snappy reveal is intentional, consider adding a comment. Otherwise, align the durations for smoother transitions.
♻️ Align timing for smoother transitions
- animate(".gemini-shape", { opacity: 1 }, { duration: 0.2 }); - animate(".gemini-core", { opacity: 0 }, { duration: 0.2 }); + animate(".gemini-shape", { opacity: 1 }, { duration: 0.5, ease: "easeInOut" }); + animate(".gemini-core", { opacity: 0 }, { duration: 0.5, ease: "easeInOut" });icons/golang-icon.tsx (2)
12-23: Verify intentional double-repeat animation.With
repeat: 1on line 20, the keyframes{ x: [0, -2, 0] }play twice, creating four position changes:0 → -2 → 0 → -2 → 0. This produces a double-bounce effect on the speed lines.If you want the speed lines to dash once (single back-and-forth), remove the repeat. If the double-bounce is intentional for emphasis, consider adding a comment.
♻️ Single dash motion (if double-bounce not intended)
animate( ".speed-line", { x: [0, -2, 0] }, { duration: 0.4, ease: "easeInOut", delay: stagger(0.1), - repeat: 1, }, );
38-66: Clean implementation with good UX patterns.The use of
cursor-pointeron line 52, staggered animations for visual interest, and clear separation between static logo elements and animated speed lines demonstrates good attention to UX and code organization.public/r/golang-icon.json (1)
1-20: Registry metadata looks correct.The registry file properly declares the
motiondependency, includes both required files (icons/golang-icon.tsxandicons/types.ts), and embeds the content correctly. The structure aligns with the shadcn registry schema.public/r/gemini-icon.json (1)
1-20: Registry structure is consistent and correct.The GeminiIcon registry file follows the same pattern as the other icon registries, correctly declares dependencies, and includes both required files with properly escaped content.
public/r/qwen-icon.json (2)
14-15: LGTM! Well-designed TypeScript interfaces.The
AnimatedIconPropsandAnimatedIconHandleinterfaces are clean, well-documented, and follow React best practices:
- Flexible sizing (number | string)
- Theme-aware color defaults
- Proper JSDoc comments
- Clear imperative handle API
1-20: Well-structured registry file with good separation of concerns.The JSON structure properly follows the shadcn registry schema, and the separation of the component logic (
qwen-icon.tsx) from shared types (types.ts) is excellent for maintainability across the seven new icons in this PR.icons/grok-icon.tsx (1)
5-52: Clean, consistent AnimatedIcon API (hover + imperative handle). (motion.dev)The
forwardRef+useImperativeHandlesetup matches the manual animation pattern, and using selector strings underuseAnimate’s scope is the right approach.Also applies to: 88-89
| "path": "icons/qwen-icon.tsx", | ||
| "content": "import { forwardRef, useImperativeHandle, useCallback } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst QwenIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 1, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = useCallback(() => {\n animate(\n scope.current,\n { rotate: 360, scale: 0.5 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\n \".qwen-shape\",\n { opacity: 0 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\".qwen-core\", { opacity: 1 }, { duration: 1, ease: \"easeInOut\" });\n }, [animate, scope]);\n\n const stop = useCallback(() => {\n animate(\n scope.current,\n { rotate: 0, scale: 1 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\".qwen-shape\", { opacity: 1 }, { duration: 0.2 });\n animate(\".qwen-core\", { opacity: 0 }, { duration: 0.2 });\n }, [animate, scope]);\n\n useImperativeHandle(\n ref,\n () => ({\n startAnimation: start,\n stopAnimation: stop,\n }),\n [start, stop],\n );\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={start}\n onHoverEnd={stop}\n fill={color}\n fillRule=\"evenodd\"\n viewBox=\"0 0 24 24\"\n height={size}\n width={size}\n style={{ flex: \"none\", lineHeight: 1 }}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n stroke={color}\n strokeWidth={strokeWidth}\n >\n <title>Qwen</title>\n\n {/* Collapse core */}\n <motion.circle\n className=\"qwen-core\"\n cx=\"12\"\n cy=\"12\"\n r=\"8\"\n opacity=\"0\"\n />\n\n {/* FULL original Qwen geometry. Do not modify this path. */}\n <motion.path\n className=\"qwen-shape\"\n d=\"M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z\"\n />\n </motion.svg>\n );\n },\n);\n\nQwenIcon.displayName = \"QwenIcon\";\nexport default QwenIcon;\n", |
There was a problem hiding this comment.
Animation timing inconsistency may cause abrupt transitions.
In the stop callback (lines visible in the embedded content), the opacity animations use a 0.2 second duration while the rotation/scale animations use 1 second. This mismatch means:
- The rotation/scale will still be animating when opacity changes complete
- The reverse animation feels 5× faster than the forward animation
- User experience may feel jarring or inconsistent
Consider using consistent durations (e.g., 1 second for all animations in stop) or explicitly staggering them if the current timing is intentional.
⚡ Suggested fix for consistent animation timing
In the stop callback within icons/qwen-icon.tsx, update the duration values:
const stop = useCallback(() => {
animate(
scope.current,
{ rotate: 0, scale: 1 },
{ duration: 1, ease: "easeInOut" },
);
- animate(".qwen-shape", { opacity: 1 }, { duration: 0.2 });
- animate(".qwen-core", { opacity: 0 }, { duration: 0.2 });
+ animate(".qwen-shape", { opacity: 1 }, { duration: 1, ease: "easeInOut" });
+ animate(".qwen-core", { opacity: 0 }, { duration: 1, ease: "easeInOut" });
}, [animate, scope]);📝 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/qwen-icon.tsx", | |
| "content": "import { forwardRef, useImperativeHandle, useCallback } from \"react\";\nimport type { AnimatedIconHandle, AnimatedIconProps } from \"./types\";\nimport { motion, useAnimate } from \"motion/react\";\n\nconst QwenIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(\n (\n { size = 24, color = \"currentColor\", strokeWidth = 1, className = \"\" },\n ref,\n ) => {\n const [scope, animate] = useAnimate();\n\n const start = useCallback(() => {\n animate(\n scope.current,\n { rotate: 360, scale: 0.5 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\n \".qwen-shape\",\n { opacity: 0 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\".qwen-core\", { opacity: 1 }, { duration: 1, ease: \"easeInOut\" });\n }, [animate, scope]);\n\n const stop = useCallback(() => {\n animate(\n scope.current,\n { rotate: 0, scale: 1 },\n { duration: 1, ease: \"easeInOut\" },\n );\n animate(\".qwen-shape\", { opacity: 1 }, { duration: 0.2 });\n animate(\".qwen-core\", { opacity: 0 }, { duration: 0.2 });\n }, [animate, scope]);\n\n useImperativeHandle(\n ref,\n () => ({\n startAnimation: start,\n stopAnimation: stop,\n }),\n [start, stop],\n );\n\n return (\n <motion.svg\n ref={scope}\n onHoverStart={start}\n onHoverEnd={stop}\n fill={color}\n fillRule=\"evenodd\"\n viewBox=\"0 0 24 24\"\n height={size}\n width={size}\n style={{ flex: \"none\", lineHeight: 1 }}\n xmlns=\"http://www.w3.org/2000/svg\"\n className={className}\n stroke={color}\n strokeWidth={strokeWidth}\n >\n <title>Qwen</title>\n\n {/* Collapse core */}\n <motion.circle\n className=\"qwen-core\"\n cx=\"12\"\n cy=\"12\"\n r=\"8\"\n opacity=\"0\"\n />\n\n {/* FULL original Qwen geometry. Do not modify this path. */}\n <motion.path\n className=\"qwen-shape\"\n d=\"M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z\"\n />\n </motion.svg>\n );\n },\n);\n\nQwenIcon.displayName = \"QwenIcon\";\nexport default QwenIcon;\n", | |
| const stop = useCallback(() => { | |
| animate( | |
| scope.current, | |
| { rotate: 0, scale: 1 }, | |
| { duration: 1, ease: "easeInOut" }, | |
| ); | |
| animate(".qwen-shape", { opacity: 1 }, { duration: 1, ease: "easeInOut" }); | |
| animate(".qwen-core", { opacity: 0 }, { duration: 1, ease: "easeInOut" }); | |
| }, [animate, scope]); |
🤖 Prompt for AI Agents
In @public/r/qwen-icon.json around lines 9 - 10, The stop callback's opacity
animations use duration 0.2 while the rotation/scale use duration 1, causing
inconsistent reverse timing; update the stop function (referencing stop,
animate, scope.current, ".qwen-shape", and ".qwen-core") to use the same
duration and easing as start (e.g., duration: 1 and ease: "easeInOut" for both
opacity animations) so rotation/scale and opacity reverse together, or
intentionally stagger them if you want a different effect.
|
On hover, the icon fully shrinks and then animates again, which personally doesn’t feel great to me. I’d like to hear your POV on this, and see if we can improve the interaction. |
|
Personally, for logos like cursor, qwen, notion, gemini, grok, and for railway, I'd like to have consistent animations. The ones there are in the cursor and qwen right now in the above videos. Currently qwen and gemini has this shrinkng on hover animation. I can change them to the one used for cursor icon or the one used for grok one. Which one is more preferrable to you? if none then i'd try to come up with something else. let me know. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @icons/gemini-icon.tsx:
- Around line 35-48: The SVG <motion.svg> lacks explicit accessibility
attributes: add either role="img" and an accessible name (aria-label or
aria-labelledby referencing the existing <title>) when the icon conveys meaning,
or make it purely decorative by adding aria-hidden="true" when it does not;
update the <motion.svg> element near the existing props (ref={scope},
onHoverStart, fill={color}, viewBox, height/width, className) to include the
appropriate attribute(s) and ensure the <title> ID matches aria-labelledby if
you choose that route.
- Around line 35-46: The SVG is missing transformBox/transformOrigin which
causes rotations/scales to pivot from the corner; update the motion.svg element
in icons/gemini-icon.tsx to include transformBox="fill-box" and
transformOrigin="center" (or set style={{ transformBox: 'fill-box',
transformOrigin: 'center' }}) so animations use the SVG's center as the
transform origin; keep existing props (ref={scope}, onHoverStart={start},
onHoverEnd={stop}, fill={color}, viewBox, height/width, className) intact.
In @icons/grok-icon.tsx:
- Around line 9-24: The start and stop animation callbacks need to respect the
user's prefers-reduced-motion setting: detect prefers-reduced-motion (e.g., via
window.matchMedia('(prefers-reduced-motion: reduce)').matches or a
useReducedMotion hook) and early-return from start/stop when reduced motion is
requested; also include that boolean in the useCallback dependency arrays so the
callbacks update when the preference changes; ensure when reduced motion is
enabled you either skip animate entirely or use a non-animating fallback (set x:
0, opacity: 1) in scope.current.
In @icons/qwen-icon.tsx:
- Around line 5-6: The component QwenIcon is typed with AnimatedIconProps which
includes strokeWidth but the prop isn't destructured or used (this is a filled
icon using fill), so either remove strokeWidth from the AnimatedIconProps type
if it only applies to stroke-based icons, or explicitly accept it in QwenIcon's
props (e.g., add strokeWidth to the destructuring for forwardRef: ({ size = 24,
color = "currentColor", strokeWidth, className = "" }, ref)) to silence the
unused prop; update either the type definition or the QwenIcon signature
accordingly and ensure no other filled icons rely on strokeWidth.
🧹 Nitpick comments (3)
icons/gemini-icon.tsx (1)
9-23: Avoid overlapping animations + guardscope.current(quick hover in/out can stack)Right now
start()/stop()can fire back-to-back and enqueue multipleanimate()calls. Consider cancelling the prior animation and no-op’ing if the ref isn’t set (depending onmotion/[email protected]’sanimate()return type).Proposed tweak (if
animate()returns stoppable controls in your Motion version)-import { forwardRef, useImperativeHandle, useCallback } from "react"; +import { forwardRef, useImperativeHandle, useCallback, useRef } from "react"; import type { AnimatedIconHandle, AnimatedIconProps } from "./types"; import { motion, useAnimate } from "motion/react"; const GeminiIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>( ({ size = 24, color = "currentColor", className = "" }, ref) => { const [scope, animate] = useAnimate(); + const controlsRef = useRef<{ stop?: () => void } | null>(null); const start = useCallback(() => { - animate( - scope.current, + if (!scope.current) return; + controlsRef.current?.stop?.(); + controlsRef.current = animate( + scope.current, { scale: [1, 1.08, 1], rotate: [0, 8, 0] }, { duration: 0.6, ease: "easeInOut" }, ); }, [animate, scope]); const stop = useCallback(() => { - animate( - scope.current, + if (!scope.current) return; + controlsRef.current?.stop?.(); + controlsRef.current = animate( + scope.current, { scale: 1, rotate: 0 }, { duration: 0.3, ease: "easeOut" }, ); }, [animate, scope]);icons/qwen-icon.tsx (1)
9-16: Consider extracting animation parameters as constants.The rotation and scale keyframes are hardcoded. For better maintainability and consistency across icon components, consider extracting these values:
const WOBBLE_ROTATION = [0, -8, 8, -4, 4, 0]; const WOBBLE_SCALE = [1, 1.05, 1]; const WOBBLE_DURATION = 0.6;This makes it easier to adjust animation behavior and maintain consistency with other icons.
Additionally, based on the PR discussion, animation consistency across icons is being evaluated. Verify that this wobble animation aligns with the final decision for icon animation patterns.
icons/grok-icon.tsx (1)
35-57: Consider addingaria-hiddenif the icon is purely decorative.If the icon is used alongside text labels or is purely decorative, adding
aria-hidden="true"to the SVG can improve accessibility by preventing screen readers from announcing redundant content. The<title>element is already present, which is good for standalone usage.♻️ Optional: Add aria-hidden support
If you want to support both decorative and semantic modes, consider:
const GrokIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>( - ({ size = 24, color = "currentColor", className = "" }, ref) => { + ({ size = 24, color = "currentColor", className = "", "aria-hidden": ariaHidden }, ref) => { const [scope, animate] = useAnimate(); // ... animation code ... return ( <motion.svg ref={scope} onHoverStart={start} onHoverEnd={stop} fill={color} fillRule="evenodd" viewBox="0 0 24 24" height={size} width={size} style={{ flex: "none", lineHeight: 1 }} xmlns="http://www.w3.org/2000/svg" className={className} + aria-hidden={ariaHidden} > <title>Grok</title>And update the types to include
aria-hidden?: booleaninAnimatedIconProps.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
icons/cursor-ide-icon.tsxicons/gemini-icon.tsxicons/grok-icon.tsxicons/notion-icon.tsxicons/qwen-icon.tsxpublic/r/cursor-ide-icon.jsonpublic/r/gemini-icon.jsonpublic/r/grok-icon.jsonpublic/r/notion-icon.jsonpublic/r/qwen-icon.json
🚧 Files skipped from review as they are similar to previous changes (6)
- public/r/cursor-ide-icon.json
- icons/cursor-ide-icon.tsx
- public/r/notion-icon.json
- public/r/qwen-icon.json
- public/r/grok-icon.json
- public/r/gemini-icon.json
🔇 Additional comments (12)
icons/notion-icon.tsx (7)
1-3: LGTM!Imports are correct. Using
motion/reactis the proper import path for Motion v12+.
5-8: LGTM!Component setup with
forwardRefanduseAnimatefollows the standard pattern for animated icon components with imperative handles.
9-22: LGTM!The page-flip animation effect is well-implemented with subtle keyframe transitions. This aligns with the PR discussion about consistent, non-jarring hover animations (avoiding the "shrink and reanimate" behavior mentioned in comments).
24-27: LGTM!The stop animation properly resets all animated properties with a snappy 0.2s duration, providing responsive hover-end feedback.
29-36: LGTM!The imperative handle correctly exposes
startAnimationandstopAnimationmethods, allowing programmatic control from parent components.
38-66: LGTM!The SVG structure is well-organized with:
- Proper
ref={scope}connection for animation targeting- Motion-specific
onHoverStart/onHoverEndhandlers- Accessible
<title>element- Correct class names on paths for animation selection
70-71: LGTM!The
displayNameaids debugging in React DevTools, and the default export is consistent with other icon components in the registry.icons/qwen-icon.tsx (3)
18-24: LGTM!The stop animation correctly resets the icon to its initial state with an appropriate shorter duration for smooth transition.
26-48: LGTM!The imperative handle is correctly implemented with proper dependencies, and the SVG element configuration is appropriate. The hover handlers are properly wired to the animation callbacks, and the
fillRule="evenodd"correctly supports the hollow center design mentioned in the comment.
49-62: LGTM!The accessibility title, SVG path structure, and component export are all correctly implemented. The
displayNamefollows React naming conventions, and the path className allows for potential styling customization.icons/grok-icon.tsx (2)
26-33: LGTM!The imperative handle is correctly implemented with proper memoization and exposes a clean API for external control of animations.
3-3: > Likely an incorrect or invalid review comment.
| const start = useCallback(() => { | ||
| // Glitch/vibrate: quick shake with opacity flicker | ||
| animate( | ||
| scope.current, | ||
| { x: [0, -2, 2, -2, 2, 0], opacity: [1, 0.8, 1, 0.8, 1] }, | ||
| { duration: 0.4, ease: "easeInOut" }, | ||
| ); | ||
| }, [animate, scope]); | ||
|
|
||
| const stop = useCallback(() => { | ||
| animate( | ||
| scope.current, | ||
| { x: 0, opacity: 1 }, | ||
| { duration: 0.15, ease: "easeOut" }, | ||
| ); | ||
| }, [animate, scope]); |
There was a problem hiding this comment.
Add support for prefers-reduced-motion to respect user accessibility preferences.
The animation does not respect the prefers-reduced-motion media query. Users with vestibular disorders or motion sensitivity should be able to disable animations.
♿ Proposed fix to add reduced motion support
+import { forwardRef, useImperativeHandle, useCallback, useMemo } from "react";
import type { AnimatedIconHandle, AnimatedIconProps } from "./types";
import { motion, useAnimate } from "motion/react";
const GrokIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
({ size = 24, color = "currentColor", className = "" }, ref) => {
const [scope, animate] = useAnimate();
+
+ const prefersReducedMotion = useMemo(() => {
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ }, []);
const start = useCallback(() => {
+ if (prefersReducedMotion) return;
+
// Glitch/vibrate: quick shake with opacity flicker
animate(
scope.current,
{ x: [0, -2, 2, -2, 2, 0], opacity: [1, 0.8, 1, 0.8, 1] },
{ duration: 0.4, ease: "easeInOut" },
);
- }, [animate, scope]);
+ }, [animate, scope, prefersReducedMotion]);
const stop = useCallback(() => {
+ if (prefersReducedMotion) return;
+
animate(
scope.current,
{ x: 0, opacity: 1 },
{ duration: 0.15, ease: "easeOut" },
);
- }, [animate, scope]);
+ }, [animate, scope, prefersReducedMotion]);🤖 Prompt for AI Agents
In @icons/grok-icon.tsx around lines 9 - 24, The start and stop animation
callbacks need to respect the user's prefers-reduced-motion setting: detect
prefers-reduced-motion (e.g., via window.matchMedia('(prefers-reduced-motion:
reduce)').matches or a useReducedMotion hook) and early-return from start/stop
when reduced motion is requested; also include that boolean in the useCallback
dependency arrays so the callbacks update when the preference changes; ensure
when reduced motion is enabled you either skip animate entirely or use a
non-animating fallback (set x: 0, opacity: 1) in scope.current.
| const QwenIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>( | ||
| ({ size = 24, color = "currentColor", className = "" }, ref) => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat icons/types.tsRepository: itshover/itshover
Length of output: 437
Remove unused strokeWidth prop from destructuring or adjust the type.
The AnimatedIconProps type includes strokeWidth, but the component doesn't destructure or use it. Since this is a filled icon (using fill rather than stroke), the strokeWidth prop is unnecessary.
Either destructure it explicitly to silence the unused prop (e.g., { size = 24, color = "currentColor", strokeWidth, className = "" }), or remove strokeWidth from AnimatedIconProps if it applies only to stroke-based icons.
🤖 Prompt for AI Agents
In @icons/qwen-icon.tsx around lines 5 - 6, The component QwenIcon is typed with
AnimatedIconProps which includes strokeWidth but the prop isn't destructured or
used (this is a filled icon using fill), so either remove strokeWidth from the
AnimatedIconProps type if it only applies to stroke-based icons, or explicitly
accept it in QwenIcon's props (e.g., add strokeWidth to the destructuring for
forwardRef: ({ size = 24, color = "currentColor", strokeWidth, className = "" },
ref)) to silence the unused prop; update either the type definition or the
QwenIcon signature accordingly and ensure no other filled icons rely on
strokeWidth.
|
@Abhijit-Jha I have made changes to the icons discussed earlier. adding after and before videos here. after.mp4before.mp4 |
|
Hey, loved the animation, but it can be improved further. Please check these references You can take inspiration from animation libraries |
|
I have updated the animation for Grok and gemini grok-gemini-animation.mp4 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @icons/grok-icon.tsx:
- Around line 32-45: The stop() animation currently sets opacity to 0.5 causing
first-hover mismatch; update the stop() function so the animate calls for
".grok-upper" and ".grok-lower" reset opacity back to 1 (or alternatively set
initial opacity="0.5" on those path elements), e.g., change the opacity target
in the animate calls inside stop() to 1 to restore the original render state;
the changes should be made in the stop() callback that also animates
scope.current.
🧹 Nitpick comments (2)
icons/grok-icon.tsx (2)
48-51: Add dependency array touseImperativeHandle.Missing dependency array causes the handle to be recreated on every render. For consistency with
gemini-icon.tsxand to avoid unnecessary updates:Suggested fix
- useImperativeHandle(ref, () => ({ + useImperativeHandle(ref, () => ({ startAnimation: start, stopAnimation: stop, - })); + }), [start, stop]);
53-65: Addroleandaria-labelfor consistent accessibility.Unlike
gemini-icon.tsx, this component is missingrole="img"andaria-label. For consistency across the icon suite and better screen reader support:Suggested fix
<motion.svg ref={scope} onHoverStart={start} onHoverEnd={stop} fill={color} viewBox="0 0 24 24" width={size} height={size} className={className} + role="img" + aria-label="Grok" style={{ flex: "none", lineHeight: 1 }} >
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
icons/gemini-icon.tsxicons/grok-icon.tsxpublic/r/gemini-icon.jsonpublic/r/grok-icon.json
🚧 Files skipped from review as they are similar to previous changes (1)
- public/r/gemini-icon.json
🔇 Additional comments (3)
icons/gemini-icon.tsx (2)
9-18: Consider the hover animation feedback from PR discussion.The reviewer noted that shrinking on hover feels "unpleasant." This animation scales down to 0.9 on hover start, which matches the reported concern. The PR discussion suggests aligning with other icons (Cursor, Qwen) or taking inspiration from the provided references (Vimeo clip at 30-32s for Gemini).
If the current animation is intentional after the "update the animation for grok and gemini" commit, this may need further iteration based on reviewer feedback.
40-68: LGTM on accessibility and SVG structure.Good implementation with
role="img",aria-label, and<title>for screen readers. ThetransformOrigin: "center"ensures rotation animates around the icon's center correctly.public/r/grok-icon.json (1)
7-18: Registry file will need regeneration after source fixes.This registry JSON embeds the source code from
icons/grok-icon.tsx. After addressing the opacity initialization,useImperativeHandledependency array, and accessibility attributes in the source file, ensure this registry file is regenerated to stay in sync.
| const stop = useCallback(() => { | ||
| animate(scope.current, { scale: 1 }, { duration: 0.2 }); | ||
|
|
||
| animate( | ||
| ".grok-upper", | ||
| { x: 0, y: 0, opacity: 0.5 }, | ||
| { duration: 0.25, ease: "easeInOut" }, | ||
| ); | ||
|
|
||
| animate( | ||
| ".grok-lower", | ||
| { x: 0, y: 0, opacity: 0.5 }, | ||
| { duration: 0.25, ease: "easeInOut" }, | ||
| ); |
There was a problem hiding this comment.
Initial opacity mismatch causes visual inconsistency.
The stop() function resets opacity to 0.5, but the paths have no initial opacity set (defaults to 1). On first hover-end, paths will dim to 50% and stay there, creating inconsistent behavior between initial render and subsequent interactions.
Either set initial opacity on the paths or reset to 1 in stop().
Suggested fix: set initial opacity on paths
{/* Upper shard */}
<motion.path
className="grok-upper"
+ initial={{ opacity: 0.5 }}
d="M9.27 15.29l7.978-5.897c.391-.29.95-.177..."
/>
{/* Lower shard */}
<motion.path
className="grok-lower"
+ initial={{ opacity: 0.5 }}
d="M7.623 16.723c-2.792-2.67-2.31-6.801..."
/>🤖 Prompt for AI Agents
In @icons/grok-icon.tsx around lines 32 - 45, The stop() animation currently
sets opacity to 0.5 causing first-hover mismatch; update the stop() function so
the animate calls for ".grok-upper" and ".grok-lower" reset opacity back to 1
(or alternatively set initial opacity="0.5" on those path elements), e.g.,
change the opacity target in the animate calls inside stop() to 1 to restore the
original render state; the changes should be made in the stop() callback that
also animates scope.current.
Added icons for
notion-grok.mp4
qwen-gemini-cursor-railway-golan.mp4
let me know which you don't like or can we improved.
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.