Skip to content

Icons/notion gwen grok golang cursor railway gemini#65

Merged
Abhijit-Jha merged 10 commits intoitshover:masterfrom
itstheanurag:icons/notion-gwen-grok-golang-cursor-railway-gemini
Jan 10, 2026
Merged

Icons/notion gwen grok golang cursor railway gemini#65
Abhijit-Jha merged 10 commits intoitshover:masterfrom
itstheanurag:icons/notion-gwen-grok-golang-cursor-railway-gemini

Conversation

@itstheanurag
Copy link
Copy Markdown
Contributor

@itstheanurag itstheanurag commented Jan 9, 2026

Added icons for

  • Golang
  • Railway
  • Gemini
  • Qwen
  • Grok
  • Cursor
  • notion
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

    • Added 7 animated icons: Cursor IDE, Gemini, Golang, Grok, Notion, Qwen, Railway.
    • Icons support hover-triggered animations, configurable size/color/stroke, and an imperative start/stop animation API.
    • Shared icon props/handle types introduced for consistent usage.
  • Chores

    • Icons registered in the public component registry and added to the icon catalog for discovery and use.

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Jan 9, 2026

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

Adds seven new forwardRef animated React icon components (Cursor IDE, Gemini, Golang, Grok, Notion, Qwen, Railway) with imperative handles and hover animations, a shared icons/types.ts, updates icon export lists, and adds registry entries.

Changes

Cohort / File(s) Summary
New Animated Icon Components
\icons/cursor-ide-icon.tsx`, `icons/gemini-icon.tsx`, `icons/golang-icon.tsx`, `icons/grok-icon.tsx`, `icons/notion-icon.tsx`, `icons/qwen-icon.tsx`, `icons/railway-icon.tsx``
Seven new default-exported forwardRef React components. Each exposes an imperative handle (startAnimation, stopAnimation) via useImperativeHandle, uses useAnimate/motion for SVG hover-driven animations, and accepts size, color, strokeWidth, className.
Shared Types
\icons/types.ts``
New exported interfaces: AnimatedIconProps (size?, color?, strokeWidth?, className?) and AnimatedIconHandle (startAnimation, stopAnimation) consumed by the icons.
Icon Export Lists
\icons/index.ts`, `lib/icons.ts``
Appended seven new ICON_LIST/ICONS entries mapping names (golang-icon, railway-icon, cursor-ide-icon, notion-icon, gemini-icon, qwen-icon, grok-icon) to the new components and keywords/paths.
Registry Entries (public)
\registry.json`, `public/r/registry.json`, `public/r/cursor-ide-icon.json`, `public/r/gemini-icon.json`, `public/r/golang-icon.json`, `public/r/grok-icon.json`, `public/r/notion-icon.json`, `public/r/qwen-icon.json`, `public/r/railway-icon.json``
Added registry:ui entries for each new icon. Each entry lists icons/<name>-icon.tsx and icons/types.ts, and declares a dependency on motion.
Minor Formatting
\public/r/hotel-icon.json`, `public/r/travel-bag.json`\`
Line-ending normalization (CRLF → LF); no behavioral changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇
I hopped in with pixel feet,
Seven icons snug and neat,
I wiggle, twirl, then take the floor,
Hover me once — I dance some more,
Tiny rabbit, tiny glow ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title lists seven icon names but lacks specificity about what was added; it reads more like a file/branch name than a clear description of the changeset's primary purpose. Consider revising the title to be more descriptive, such as 'Add animated icon components for Golang, Railway, Gemini, Qwen, Grok, Cursor, and Notion' to clearly convey that new animated icon components were added.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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-inner for outer, railway-outer for 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 adding transformBox/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 set aria-hidden if 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0057496 and 099a78b.

📒 Files selected for processing (20)
  • icons/cursor-ide-icon.tsx
  • icons/gemini-icon.tsx
  • icons/golang-icon.tsx
  • icons/grok-icon.tsx
  • icons/index.ts
  • icons/notion-icon.tsx
  • icons/qwen-icon.tsx
  • icons/railway-icon.tsx
  • lib/icons.ts
  • public/r/cursor-ide-icon.json
  • public/r/gemini-icon.json
  • public/r/golang-icon.json
  • public/r/grok-icon.json
  • public/r/hotel-icon.json
  • public/r/notion-icon.json
  • public/r/qwen-icon.json
  • public/r/railway-icon.json
  • public/r/registry.json
  • public/r/travel-bag.json
  • registry.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 motion and includes both the component and types files.

Note: The embedded component code has the same useImperativeHandle missing dependency array issue identified in other icon files. Ensure the source file icons/golang-icon.tsx is updated and this registry JSON is regenerated.

icons/gemini-icon.tsx (1)

9-43: No action needed. The animate(".gemini-*") selectors are automatically scoped by motion/react to descendants of scope.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's useAnimate() 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 the ref={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 startAnimation and stopAnimation methods.

registry.json (1)

1163-1179: Auto-generated registry entries look correct.

The seven new icon entries follow the established structure with proper dependencies on motion and correct file references. Since this is an auto-generated file, ensure npm run registry:build was 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.tsx file.

public/r/registry.json (1)

1163-1179: Auto-generated registry entries are consistent.

The seven new icon entries in public/r/registry.json match those in the root registry.json. Both files are auto-generated and properly synchronized.

Also applies to: 1554-1570, 1673-1706, 2166-2182, 2387-2403, 2421-2437

Comment thread icons/cursor-ide-icon.tsx Outdated
Comment thread icons/notion-icon.tsx Outdated
Comment thread icons/qwen-icon.tsx Outdated
Comment thread icons/railway-icon.tsx Outdated
Comment thread public/r/cursor-ide-icon.json
Comment on lines +1 to +20
{
"$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"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 startAnimation or stopAnimation are called imperatively via the ref handle before the component mounts, scope.current will be null, 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.current will be null.

🛡️ 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 stroke and strokeWidth attributes 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 the strokeWidth prop) 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.current explicitly
  • 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 strokeWidth from props, but AnimatedIconProps includes it. Since GeminiIcon uses fill rather than stroke, strokeWidth isn'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-pointer in its className (see golang-icon.tsx line 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-pointer class 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. Setting transformOrigin (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 an ariaLabel prop and defaulting to aria-hidden when absent.


58-82: Consider moving fillRule="evenodd" to the <path> for clarity. (developer.mozilla.org)

fill-rule is 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's animate() returns AnimationControls with 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

📥 Commits

Reviewing files that changed from the base of the PR and between 099a78b and 5a35782.

📒 Files selected for processing (14)
  • icons/cursor-ide-icon.tsx
  • icons/gemini-icon.tsx
  • icons/golang-icon.tsx
  • icons/grok-icon.tsx
  • icons/notion-icon.tsx
  • icons/qwen-icon.tsx
  • icons/railway-icon.tsx
  • public/r/cursor-ide-icon.json
  • public/r/gemini-icon.json
  • public/r/golang-icon.json
  • public/r/grok-icon.json
  • public/r/notion-icon.json
  • public/r/qwen-icon.json
  • public/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 stop callback 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: 1 on 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-pointer on 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 motion dependency, includes both required files (icons/golang-icon.tsx and icons/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 AnimatedIconProps and AnimatedIconHandle interfaces 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 + useImperativeHandle setup matches the manual animation pattern, and using selector strings under useAnimate’s scope is the right approach.

Also applies to: 88-89

Comment thread icons/qwen-icon.tsx
Comment thread public/r/qwen-icon.json Outdated
Comment on lines +9 to +10
"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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
"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.

@Abhijit-Jha
Copy link
Copy Markdown
Member

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.

@itstheanurag
Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 + guard scope.current (quick hover in/out can stack)

Right now start() / stop() can fire back-to-back and enqueue multiple animate() calls. Consider cancelling the prior animation and no-op’ing if the ref isn’t set (depending on motion/[email protected]’s animate() 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 adding aria-hidden if 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?: boolean in AnimatedIconProps.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a35782 and 7e0dad0.

📒 Files selected for processing (10)
  • icons/cursor-ide-icon.tsx
  • icons/gemini-icon.tsx
  • icons/grok-icon.tsx
  • icons/notion-icon.tsx
  • icons/qwen-icon.tsx
  • public/r/cursor-ide-icon.json
  • public/r/gemini-icon.json
  • public/r/grok-icon.json
  • public/r/notion-icon.json
  • public/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/react is the proper import path for Motion v12+.


5-8: LGTM!

Component setup with forwardRef and useAnimate follows 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 startAnimation and stopAnimation methods, 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/onHoverEnd handlers
  • Accessible <title> element
  • Correct class names on paths for animation selection

70-71: LGTM!

The displayName aids 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 displayName follows 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.

Comment thread icons/gemini-icon.tsx
Comment thread icons/gemini-icon.tsx
Comment thread icons/grok-icon.tsx
Comment on lines +9 to +24
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]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment thread icons/qwen-icon.tsx
Comment on lines +5 to +6
const QwenIcon = forwardRef<AnimatedIconHandle, AnimatedIconProps>(
({ size = 24, color = "currentColor", className = "" }, ref) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat icons/types.ts

Repository: 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.

@itstheanurag
Copy link
Copy Markdown
Contributor Author

@Abhijit-Jha I have made changes to the icons discussed earlier. adding after and before videos here.

after.mp4
before.mp4

@Abhijit-Jha
Copy link
Copy Markdown
Member

Hey, loved the animation, but it can be improved further. Please check these references
Check this video around the 30–32 second mark to see the Gemini icon animation:
https://vimeo.com/913062540
For Grok, refer to this animation:
https://iconscout.com/lottie-animation/grok-logo-animation_11682693

You can take inspiration from animation libraries

@itstheanurag
Copy link
Copy Markdown
Contributor Author

I have updated the animation for Grok and gemini

grok-gemini-animation.mp4

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 to useImperativeHandle.

Missing dependency array causes the handle to be recreated on every render. For consistency with gemini-icon.tsx and to avoid unnecessary updates:

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

53-65: Add role and aria-label for consistent accessibility.

Unlike gemini-icon.tsx, this component is missing role="img" and aria-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

📥 Commits

Reviewing files that changed from the base of the PR and between 7e0dad0 and b97d630.

📒 Files selected for processing (4)
  • icons/gemini-icon.tsx
  • icons/grok-icon.tsx
  • public/r/gemini-icon.json
  • public/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. The transformOrigin: "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, useImperativeHandle dependency array, and accessibility attributes in the source file, ensure this registry file is regenerated to stay in sync.

Comment thread icons/grok-icon.tsx
Comment on lines +32 to +45
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" },
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@Abhijit-Jha Abhijit-Jha merged commit e02371b into itshover:master Jan 10, 2026
2 of 3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 13, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants