Skip to content

feat: add ProfileDropdown component with animated icons and avatar fallback#72

Open
vxnsh1 wants to merge 8 commits intoitshover:masterfrom
vxnsh1:vxnsh1/profile-dropdown
Open

feat: add ProfileDropdown component with animated icons and avatar fallback#72
vxnsh1 wants to merge 8 commits intoitshover:masterfrom
vxnsh1:vxnsh1/profile-dropdown

Conversation

@vxnsh1
Copy link
Copy Markdown

@vxnsh1 vxnsh1 commented Jan 11, 2026

Add an interactive profile dropdown with motion effects and avatar fallback.

Profile.Dropdown.Demo.mp4

Summary by CodeRabbit

  • New Features

    • Profile dropdown: avatar button with fallback, smooth open/close animations, keyboard & click-outside dismissal, account actions with badges and per-item animated icons.
    • New animated icons: credit card, gear, logout, message, and user-plus — support hover and programmatic animation control.
  • Examples

    • Added a Profile Dropdown entry to the examples gallery.
  • Chores

    • Updated motion dependency version.

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

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Jan 11, 2026

@vxnsh1 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 11, 2026

📝 Walkthrough

Walkthrough

Adds a new client-side ProfileDropdown component with animated per-item icons and accessibility features, registers it in EXAMPLE_REGISTRY, introduces five forwardRef animated SVG icon components exposing start/stop animation handles, and bumps the motion dependency.

Changes

Cohort / File(s) Summary
Profile dropdown component
components/examples/profile-dropdown.tsx
New default export ProfileDropdown: avatar button toggles an accessible dropdown with five items (Profile, Community, Subscription with PRO badge, Settings, Sign Out). Uses local state, closes on Escape/outside click, optional avatarSrc fallback, Framer Motion enter/exit, internal ProfileDropdownItem and per-item animated icons.
Example registry
lib/examples.ts
Imported ProfileDropdown and added an EXAMPLE_REGISTRY entry (profile-dropdown) with metadata (componentName, slug, createdBy, filePath, component, description, tags).
Animated icon components
components/ui/credit-card.tsx, components/ui/gear-icon.tsx, components/ui/logout-icon.tsx, components/ui/message-circle-icon.tsx, components/ui/user-plus-icon.tsx
Five new default-export forwardRef animated SVG components. Each exposes startAnimation / stopAnimation via an AnimatedIconHandle, uses Framer Motion/useAnimate, supports hover-triggered animation (configurable via disableHover), and accepts size, color, strokeWidth, and className props.
Dependency bump
package.json
Updated motion dependency from ^12.23.26 to ^12.25.0.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProfileDropdown
    participant AnimatedIcon
    participant FramerMotion
    participant Document

    User->>ProfileDropdown: Click avatar button
    ProfileDropdown->>ProfileDropdown: set isOpen = true
    ProfileDropdown->>FramerMotion: play open (enter) animation
    FramerMotion-->>ProfileDropdown: visible
    User->>ProfileDropdown: Hover dropdown item
    ProfileDropdown->>AnimatedIcon: startAnimation (if isAnimated)
    AnimatedIcon-->>ProfileDropdown: animation running
    User->>Document: Click outside / press Escape
    Document->>ProfileDropdown: outside-click / keydown event
    ProfileDropdown->>FramerMotion: play close (exit) animation
    FramerMotion-->>ProfileDropdown: hidden
    ProfileDropdown->>ProfileDropdown: set isOpen = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I tap an avatar, gates unfold,
Icons wiggle, tiny stories told,
A PRO badge winks, the menu plays,
Motion hums through friendly ways,
I hop away — the code now bold ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a ProfileDropdown component with animated icons and avatar fallback, which directly matches the primary file addition and functionality described in the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 11, 2026

Greptile Overview

Greptile Summary

This PR adds a new ProfileDropdown component with Framer Motion animations, avatar fallback support, and badge functionality for menu items.

Key Changes:

  • Added interactive profile dropdown with smooth open/close animations
  • Implemented animated icon support for each menu item
  • Added default avatar fallback image (/default-avatar.jpg)
  • Registered component in examples registry

Critical Issues Found:

  • Icons imported from @/icons/ don't support the disableHover prop being passed on line 52, which will cause TypeScript errors or runtime issues
  • Types imported from ../ui/types but icons from @/icons/ - inconsistent import paths that may cause type mismatches
  • Component uses native img tag instead of Next.js Image component for optimization
  • Missing accessibility attributes (aria-label, aria-expanded) on dropdown button

Confidence Score: 2/5

  • This PR has critical type compatibility issues that will likely cause runtime errors or TypeScript compilation failures.
  • Score reflects critical issues with icon imports and type compatibility. The icons from @/icons/ directory don't support the disableHover prop used throughout the component, which will break functionality. The type imports are also inconsistent with icon imports, creating potential type mismatches.
  • Pay close attention to components/examples/profile-dropdown.tsx - the icon import paths and disableHover prop usage must be fixed before merging.

Important Files Changed

File Analysis

Filename Score Overview
components/examples/profile-dropdown.tsx 2/5 New ProfileDropdown component added with animation support. Critical issues: icons imported from @/icons/ don't support disableHover prop, causing type/runtime errors. Types imported from wrong path.
lib/examples.ts 5/5 Added ProfileDropdown to examples registry with proper metadata and tags. No issues found.
public/default-avatar.jpg 5/5 Added default avatar image for fallback. Binary file, no issues detected.

Sequence Diagram

sequenceDiagram
    participant User
    participant ProfileDropdown
    participant Button
    participant AnimatePresence
    participant DropdownMenu
    participant ProfileDropdownItem
    participant AnimatedIcon
    
    User->>Button: Click avatar
    Button->>ProfileDropdown: Toggle isOpen state
    ProfileDropdown->>AnimatePresence: Render with isOpen=true
    AnimatePresence->>DropdownMenu: Animate entry (opacity, y, scale)
    
    loop For each menu item
        DropdownMenu->>ProfileDropdownItem: Render item
        ProfileDropdownItem->>AnimatedIcon: Pass ref and disableHover prop
    end
    
    User->>ProfileDropdownItem: Mouse enter
    ProfileDropdownItem->>AnimatedIcon: Call startAnimation()
    AnimatedIcon->>AnimatedIcon: Execute animation
    
    User->>ProfileDropdownItem: Mouse leave
    ProfileDropdownItem->>AnimatedIcon: Call stopAnimation()
    AnimatedIcon->>AnimatedIcon: Reset animation
    
    User->>Document: Click outside
    Document->>ProfileDropdown: mousedown event listener
    ProfileDropdown->>AnimatePresence: Set isOpen=false
    AnimatePresence->>DropdownMenu: Animate exit (opacity, y, scale)
    DropdownMenu->>ProfileDropdown: Unmount
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +7 to +11
import UserPlusIcon from "@/icons/user-plus-icon";
import GearIcon from "@/icons/gear-icon";
import MessageCircleIcon from "@/icons/message-circle-icon";
import CreditCard from "@/icons/credit-card";
import LogoutIcon from "@/icons/logout-icon";
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.

icons from @/icons/ don't support the disableHover prop used on line 52. Either import from ../ui/ (which supports disableHover) or remove the prop usage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 7:11

Comment:
icons from `@/icons/` don't support the `disableHover` prop used on line 52. Either import from `../ui/` (which supports `disableHover`) or remove the prop usage.

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

Comment thread components/examples/profile-dropdown.tsx
Comment on lines +107 to +111
<img
src={avatarSrc || "/default-avatar.jpg"}
alt="User avatar"
className="h-10 w-10 rounded-full object-cover"
/>
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.

using native img tag in Next.js. Consider using next/image for optimized image loading and better performance.

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

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 107:111

Comment:
using native `img` tag in Next.js. Consider using `next/image` for optimized image loading and better performance.

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

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

Comment on lines +103 to +112
<button
onClick={() => setIsOpen((prev) => !prev)}
className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
>
<img
src={avatarSrc || "/default-avatar.jpg"}
alt="User avatar"
className="h-10 w-10 rounded-full object-cover"
/>
</button>
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.

add aria-label and aria-expanded attributes for better accessibility.

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

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 103:112

Comment:
add `aria-label` and `aria-expanded` attributes for better accessibility.

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

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @components/examples/profile-dropdown.tsx:
- Around line 103-112: The dropdown toggle currently only supports mouse clicks;
add keyboard handling so Escape closes the menu and ArrowUp/ArrowDown navigate
items: introduce a focusIndex state and an array/ref of item refs (e.g.,
itemRefs), add a handleKeyDown function attached to the menu container or the
toggle button that listens for 'Escape' to call setIsOpen(false) and focus the
toggle button, and listens for 'ArrowDown'/'ArrowUp' to increment/decrement
focusIndex and call itemRefs[focusIndex]?.focus(); ensure focusIndex resets when
the menu opens/closes and that menu items (the interactive elements rendered
inside the dropdown) have tabIndex and ref wired to itemRefs so keyboard
navigation works smoothly.
🧹 Nitpick comments (4)
components/examples/profile-dropdown.tsx (4)

57-59: Consider making badge styling configurable.

The badge uses hardcoded purple colors (bg-purple-200, text-purple-800). Consider accepting badge styling as a prop to make the component more flexible.

♻️ Proposed enhancement
 interface ProfileDropdownItemProps {
   icon: React.ComponentType<
     AnimatedIconProps & React.RefAttributes<AnimatedIconHandle>
   >;
   label: string;
   href: string;
   badge?: string;
+  badgeClassName?: string;
   isAnimated?: boolean;
 }

 const ProfileDropdownItem = ({
   icon: Icon,
   label,
   href,
   badge,
+  badgeClassName = "rounded-full bg-purple-200 px-2 py-0.5 text-xs font-semibold text-purple-800",
   isAnimated = true,
 }: ProfileDropdownItemProps) => {
   // ... rest of component

   {badge && (
-    <span className="rounded-full bg-purple-200 px-2 py-0.5 text-xs font-semibold text-purple-800">
+    <span className={badgeClassName}>
       {badge}
     </span>
   )}

77-89: Consider making menu items configurable.

The menu items are hardcoded within the component. For better reusability, consider accepting items as a prop with sensible defaults.

♻️ Proposed enhancement
 interface ProfileDropdownProps {
   avatarSrc?: string;
   isAnimated?: boolean;
+  items?: ProfileDropdownItemProps[];
 }

 const ProfileDropdown = ({
   avatarSrc,
   isAnimated = true,
+  items: customItems,
 }: ProfileDropdownProps) => {
   const [isOpen, setIsOpen] = useState(false);
   const dropdownRef = useRef<HTMLDivElement>(null);

-  const items: ProfileDropdownItemProps[] = [
+  const defaultItems: ProfileDropdownItemProps[] = [
     { icon: UserPlusIcon, label: "Profile", href: "#", isAnimated },
     { icon: MessageCircleIcon, label: "Community", href: "#", isAnimated },
     {
       icon: CreditCard,
       label: "Subscription",
       href: "#",
       badge: "PRO",
       isAnimated,
     },
     { icon: GearIcon, label: "Settings", href: "#", isAnimated },
     { icon: LogoutIcon, label: "Sign Out", href: "#", isAnimated },
   ];

+  const items = customItems || defaultItems;

107-111: Add error handling for avatar image loading.

Consider adding an onError handler to gracefully handle cases where the avatar image fails to load.

♻️ Proposed enhancement
+  const [avatarError, setAvatarError] = useState(false);
+
   return (
     <div ref={dropdownRef} className="relative inline-block">
       <button
         onClick={() => setIsOpen((prev) => !prev)}
         className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
       >
-        <img
-          src={avatarSrc || "/default-avatar.jpg"}
-          alt="User avatar"
-          className="h-10 w-10 rounded-full object-cover"
-        />
+        {!avatarError ? (
+          <img
+            src={avatarSrc || "/default-avatar.jpg"}
+            alt="User avatar"
+            className="h-10 w-10 rounded-full object-cover"
+            onError={() => setAvatarError(true)}
+          />
+        ) : (
+          <div className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-200">
+            <span className="text-sm font-semibold text-gray-600">?</span>
+          </div>
+        )}
       </button>

123-125: Consider closing dropdown on item click.

For better UX, the dropdown should close when a user clicks on a menu item. Currently, it remains open after navigation.

♻️ Proposed enhancement
           >
             {items.map((item) => (
-              <ProfileDropdownItem key={item.label} {...item} />
+              <ProfileDropdownItem 
+                key={item.label} 
+                {...item} 
+                onClick={() => setIsOpen(false)}
+              />
             ))}
           </motion.div>

And update ProfileDropdownItem to accept and use the onClick:

 interface ProfileDropdownItemProps {
   icon: React.ComponentType<
     AnimatedIconProps & React.RefAttributes<AnimatedIconHandle>
   >;
   label: string;
   href: string;
   badge?: string;
   isAnimated?: boolean;
+  onClick?: () => void;
 }

 const ProfileDropdownItem = ({
   icon: Icon,
   label,
   href,
   badge,
   isAnimated = true,
+  onClick,
 }: ProfileDropdownItemProps) => {
   // ... existing code ...
   
   return (
     <Link
       href={href}
       className="group hover:bg-accent hover:text-accent-foreground flex items-center justify-between gap-3 rounded-md px-4 py-2 transition-colors"
       onMouseEnter={handleMouseEnter}
       onMouseLeave={handleMouseLeave}
+      onClick={onClick}
     >
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1239d and 74f73c6.

⛔ Files ignored due to path filters (1)
  • public/default-avatar.jpg is excluded by !**/*.jpg
📒 Files selected for processing (2)
  • components/examples/profile-dropdown.tsx
  • lib/examples.ts
🔇 Additional comments (5)
lib/examples.ts (1)

6-6: LGTM! Registry entry is well-structured.

The import and registry entry follow the existing pattern correctly. The metadata (description, tags) accurately reflects the component's functionality.

Also applies to: 47-56

components/examples/profile-dropdown.tsx (4)

91-99: LGTM! Click-outside handler is correctly implemented.

The event listener properly handles clicks outside the dropdown and cleans up on unmount.


107-111: No action needed — default avatar asset exists.

The /default-avatar.jpg file is present in the public directory (8273 bytes). The fallback avatar path is valid and will not result in a broken image.


5-5: The type imports from ../ui/types are correctly exported. Both AnimatedIconHandle and AnimatedIconProps are defined in components/ui/types.ts and the import path is valid.


7-11: All icon imports are correctly implemented. Each icon component properly exports using forwardRef with the AnimatedIconProps interface and AnimatedIconHandle ref support, and they are correctly used in the component.

Comment on lines +103 to +112
<button
onClick={() => setIsOpen((prev) => !prev)}
className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
>
<img
src={avatarSrc || "/default-avatar.jpg"}
alt="User avatar"
className="h-10 w-10 rounded-full object-cover"
/>
</button>
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 keyboard accessibility support.

The dropdown lacks keyboard navigation support. Users should be able to close the dropdown with the Escape key and navigate items with arrow keys for better accessibility.

♻️ Proposed enhancement
   useEffect(() => {
     const handler = (e: MouseEvent) => {
       if (!dropdownRef.current?.contains(e.target as Node)) {
         setIsOpen(false);
       }
     };
     document.addEventListener("mousedown", handler);
     return () => document.removeEventListener("mousedown", handler);
   }, []);

+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === "Escape" && isOpen) {
+        setIsOpen(false);
+      }
+    };
+    document.addEventListener("keydown", handleKeyDown);
+    return () => document.removeEventListener("keydown", handleKeyDown);
+  }, [isOpen]);

   return (
     <div ref={dropdownRef} className="relative inline-block">
       <button
         onClick={() => setIsOpen((prev) => !prev)}
+        aria-expanded={isOpen}
+        aria-haspopup="true"
+        aria-label="Open profile menu"
         className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
       >
🤖 Prompt for AI Agents
In @components/examples/profile-dropdown.tsx around lines 103 - 112, The
dropdown toggle currently only supports mouse clicks; add keyboard handling so
Escape closes the menu and ArrowUp/ArrowDown navigate items: introduce a
focusIndex state and an array/ref of item refs (e.g., itemRefs), add a
handleKeyDown function attached to the menu container or the toggle button that
listens for 'Escape' to call setIsOpen(false) and focus the toggle button, and
listens for 'ArrowDown'/'ArrowUp' to increment/decrement focusIndex and call
itemRefs[focusIndex]?.focus(); ensure focusIndex resets when the menu
opens/closes and that menu items (the interactive elements rendered inside the
dropdown) have tabIndex and ref wired to itemRefs so keyboard navigation works
smoothly.

@Abhijit-Jha
Copy link
Copy Markdown
Member

Nice, can you hover over the icons(not text) while keeping the animation toggle off, and share the video as well?

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

🧹 Nitpick comments (4)
components/examples/profile-dropdown.tsx (4)

33-39: Wrap handlers in useCallback for performance.

The handleMouseEnter and handleMouseLeave functions are recreated on every render. While this won't cause functional issues, wrapping them in useCallback would prevent unnecessary re-creations.

♻️ Optimize handlers with useCallback
+import React, { useState, useRef, useEffect, useCallback } from "react";

...

  const ref = useRef<AnimatedIconHandle>(null);

- const handleMouseEnter = () => {
+ const handleMouseEnter = useCallback(() => {
    if (isAnimated) ref.current?.startAnimation();
- };
+ }, [isAnimated]);

- const handleMouseLeave = () => {
+ const handleMouseLeave = useCallback(() => {
    if (isAnimated) ref.current?.stopAnimation();
- };
+ }, [isAnimated]);

41-43: Consider consolidating animation control logic.

The useEffect calls stopAnimation() whenever isAnimated becomes false, which is correct. However, consider whether starting the animation when isAnimated becomes true again is desired behavior, as currently it only stops but never auto-starts.


57-61: Consider making badge styling configurable.

The badge uses hardcoded purple colors (bg-purple-200, text-purple-800). Consider accepting badge color variants as a prop if multiple badge types will be needed in the future.


78-90: Memoize the items array to prevent unnecessary re-renders.

The items array is recreated on every render, which can cause unnecessary re-renders of child components. Since it only depends on isAnimated, wrap it in useMemo.

♻️ Memoize items array
+import React, { useState, useRef, useEffect, useMemo } from "react";

...

- const items: ProfileDropdownItemProps[] = [
+ const items: ProfileDropdownItemProps[] = useMemo(() => [
    { icon: UserPlusIcon, label: "Profile", href: "#", isAnimated },
    { icon: MessageCircleIcon, label: "Community", href: "#", isAnimated },
    {
      icon: CreditCard,
      label: "Subscription",
      href: "#",
      badge: "PRO",
      isAnimated,
    },
    { icon: GearIcon, label: "Settings", href: "#", isAnimated },
    { icon: LogoutIcon, label: "Sign Out", href: "#", isAnimated },
- ];
+ ], [isAnimated]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74f73c6 and ee7d877.

📒 Files selected for processing (1)
  • components/examples/profile-dropdown.tsx
🔇 Additional comments (5)
components/examples/profile-dropdown.tsx (5)

1-22: LGTM!

The imports and type definitions are well-structured. The use of proper TypeScript types for animated icons and the "use client" directive for Next.js 15 client-side interactivity is appropriate.


79-89: Placeholder hrefs should be replaced with actual routes.

All menu items use "#" as the href value. These should be updated with actual route paths or documented as intentional placeholders for the example component.

Do these placeholder hrefs need to be updated, or are they intentional for this example component?


92-108: Event listener cleanup is correct.

Both useEffect hooks properly clean up their event listeners, and the empty dependency arrays are appropriate since the handlers only set state and don't depend on external values.


112-125: Good accessibility implementation.

The avatar button includes proper ARIA attributes (aria-label and aria-expanded) for screen readers, and the Image component is correctly configured with alt text.


127-141: Smooth animation implementation.

The Framer Motion integration with AnimatePresence provides clean enter/exit animations for the dropdown. The animation parameters (duration, scale, opacity) create a polished user experience.

@vxnsh1
Copy link
Copy Markdown
Author

vxnsh1 commented Jan 11, 2026

Nice, can you hover over the icons(not text) while keeping the animation toggle off, and share the video as well?

Just to clarify when the animation toggle is off the global animation works as expected, but hovering over the icons still triggers the animation. Is this intended behavior or should animations be completely off?

@Abhijit-Jha
Copy link
Copy Markdown
Member

You can turn it off check this PR - 6e1239d

Copy link
Copy Markdown
Author

@vxnsh1 vxnsh1 left a comment

Choose a reason for hiding this comment

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

Small thing I ran into: ui/ icons have a disableHover prop to turn off hover animations, but the icons from @/icons don’t. Because of that mismatch, I handled stopping hover animations inside the dropdown itself. If you’d rather keep everything using ui/ icons only, I can switch it.

Profile.Dropdown.Fix.Demo.mp4

@Abhijit-Jha
Copy link
Copy Markdown
Member

You don’t have to use icons from the @iCons folder, as I mentioned in the issue. Please check the 3rd point here: #60

Copy link
Copy Markdown
Author

@vxnsh1 vxnsh1 left a comment

Choose a reason for hiding this comment

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

Updated ProfileDropdown to use ui icons instead of icons/. Passed disableHover to each icon so that when animation is turned off, hovering over the icon will not trigger the animation.

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

🧹 Nitpick comments (6)
components/examples/profile-dropdown.tsx (2)

6-6: Inconsistent motion import path.

This file imports from "framer-motion" while the icon components (e.g., message-circle-icon.tsx, gear-icon.tsx) import from "motion/react". For consistency and to ensure the same module instance is used, consider aligning the import path.

Suggested fix
-import { motion, AnimatePresence } from "framer-motion";
+import { motion, AnimatePresence } from "motion/react";

115-128: Add aria-haspopup for improved accessibility.

The button has good accessibility attributes, but adding aria-haspopup="menu" would better communicate the dropdown's purpose to screen readers.

Suggested fix
       <button
         onClick={() => setIsOpen((prev) => !prev)}
         aria-label="Toggle profile menu"
         aria-expanded={isOpen}
+        aria-haspopup="menu"
         className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
       >
components/ui/message-circle-icon.tsx (1)

42-48: Remove unused functions.

handleHoverStart and handleHoverEnd are defined but never used. The onHoverStart and onHoverEnd props directly reference start and stop.

Suggested fix
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
components/ui/gear-icon.tsx (1)

39-45: Remove unused functions.

handleHoverStart and handleHoverEnd are defined but never used, similar to message-circle-icon.tsx. The hover handlers directly reference start and stop.

Suggested fix
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
components/ui/credit-card.tsx (2)

12-52: Wrap start and stop in useCallback for consistency and stability.

Unlike LogoutIcon which correctly uses useCallback, these functions are recreated on every render. This creates unstable references for the imperative handle and is inconsistent with the other animated icon components in this PR.

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

Then wrap the functions:

-    const start = async () => {
+    const start = useCallback(async () => {
       animate(
         ".card-body",
         // ... animation config
       );
       // ... rest of animation
-    };
+    }, [animate]);

-    const stop = () => {
+    const stop = useCallback(() => {
       animate(".card-body", { rotate: 0, scale: 1 }, { duration: 0.2 });
       // ... rest
-    };
+    }, [animate]);

61-67: Remove unused handler functions.

handleHoverStart and handleHoverEnd are defined but never used—the JSX directly references start and stop on lines 72-73. Remove this dead code.

Suggested fix
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee7d877 and c682bcb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • components/examples/profile-dropdown.tsx
  • components/ui/credit-card.tsx
  • components/ui/gear-icon.tsx
  • components/ui/logout-icon.tsx
  • components/ui/message-circle-icon.tsx
  • components/ui/user-plus-icon.tsx
  • package.json
🔇 Additional comments (8)
components/examples/profile-dropdown.tsx (3)

37-81: Well-implemented animation control.

The component correctly implements the animation toggle behavior discussed in the PR:

  • Manual onMouseEnter/onMouseLeave handlers check isAnimated before triggering animations
  • The useEffect ensures running animations stop when isAnimated changes to false
  • The disableHover prop on the Icon provides an additional layer of control

This belt-and-suspenders approach ensures animations are reliably disabled when the toggle is off.


130-147: LGTM!

The dropdown animation using AnimatePresence and motion.div is well-structured with appropriate transition values.


121-127: Default avatar exists in public directory.

The fallback image /default-avatar.jpg is present at public/default-avatar.jpg and will load correctly without 404 errors.

components/ui/message-circle-icon.tsx (1)

5-40: LGTM!

The animation logic with path drawing and scale pulse effects is well-implemented. The imperative handle correctly exposes startAnimation and stopAnimation methods.

components/ui/user-plus-icon.tsx (1)

5-112: LGTM!

Well-structured animated icon with proper use of useCallback for memoized animation handlers. The two-part animation (user avatar bounce + plus sign rotation) creates a cohesive effect, and the disableHover prop is correctly implemented.

components/ui/gear-icon.tsx (1)

47-82: LGTM!

The gear animation with rotating parts and the transformOrigin/transformBox setup for proper pivot behavior is well-implemented.

package.json (1)

31-31: Motion version bump is stable and safe.

The update to ^12.25.0 is the official latest release with no known vulnerabilities or breaking changes. This aligns well with the new animated icon components.

components/ui/logout-icon.tsx (1)

1-82: LGTM!

The component follows the established animated icon pattern correctly:

  • Uses useCallback for memoized animation handlers
  • Properly exposes startAnimation/stopAnimation via imperative handle
  • Correctly disables hover animations when disableHover is true
  • Animation sequencing (arrow → door) provides a nice visual effect

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 @components/examples/profile-dropdown.tsx:
- Around line 64-80: The Icon's internal hover handlers can double-trigger
animations because both Link's handlers (handleMouseEnter/handleMouseLeave) and
Icon call start/stop when isAnimated is true; update the Icon usage in
profile-dropdown.tsx to always pass disableHover={true} (i.e., Icon ...
disableHover={true}) and remove reliance on Icon's hover behavior so the parent
Link's onMouseEnter/onMouseLeave exclusively control animations; apply the same
change to the similar occurrence in x-sidebar.tsx to prevent concurrent
animation calls.
🧹 Nitpick comments (4)
components/ui/credit-card.tsx (1)

67-73: Remove unused handler functions.

handleHoverStart and handleHoverEnd are defined but never used—Lines 78-79 pass start and stop directly to onHoverStart/onHoverEnd.

🧹 Proposed cleanup
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
components/ui/message-circle-icon.tsx (1)

48-54: Remove unused handler functions.

Same as in credit-card.tsx: handleHoverStart and handleHoverEnd are defined but never called—Lines 59-60 pass start and stop directly.

🧹 Proposed cleanup
-    const handleHoverStart = () => {
-      start();
-    };
-
-    const handleHoverEnd = () => {
-      stop();
-    };
-
     return (
components/examples/profile-dropdown.tsx (2)

6-6: Import inconsistency: use motion/react for consistency.

Other icon components in this PR import from motion/react, but this file uses framer-motion. For consistency across the codebase, consider using the same import path.

♻️ Suggested change
-import { motion, AnimatePresence } from "framer-motion";
+import { motion, AnimatePresence } from "motion/react";

139-145: Consider using a more stable key.

Using item.label as a key works if labels are unique, but adding an explicit id field to items would be more robust against future label changes or duplicates.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c682bcb and e2f890f.

📒 Files selected for processing (6)
  • components/examples/profile-dropdown.tsx
  • components/ui/credit-card.tsx
  • components/ui/gear-icon.tsx
  • components/ui/logout-icon.tsx
  • components/ui/message-circle-icon.tsx
  • components/ui/user-plus-icon.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/ui/gear-icon.tsx
  • components/ui/logout-icon.tsx
🔇 Additional comments (5)
components/ui/credit-card.tsx (1)

75-110: LGTM!

The component correctly implements the AnimatedIconHandle interface with forwardRef and useImperativeHandle. The hover behavior respects disableHover, and the SVG animation setup with multiple animated paths is well-structured.

components/ui/message-circle-icon.tsx (1)

56-82: LGTM!

The component correctly implements the animated icon pattern with pathLength animation. The initial state is properly set, and the disableHover prop is respected.

components/examples/profile-dropdown.tsx (1)

95-111: LGTM on accessibility and event handling.

The dropdown correctly handles Escape key and outside clicks. ARIA attributes (aria-label, aria-expanded) are properly set on the button.

components/ui/user-plus-icon.tsx (2)

18-44: Good use of useCallback for animation handlers.

This component correctly wraps start and stop in useCallback with [animate] as the dependency. This is cleaner than the other icon components that don't memoize these functions—consider applying this pattern consistently across all animated icons.


77-113: LGTM!

Clean implementation with no dead code. The SVG structure with separate animated groups for user-avatar and plus-sign is well-organized, and transformOrigin is properly set for each animation target.

Comment thread components/examples/profile-dropdown.tsx
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: 0

🧹 Nitpick comments (3)
components/examples/profile-dropdown.tsx (3)

115-128: Consider adding aria-haspopup for better accessibility.

The button has good accessibility attributes (aria-label, aria-expanded), but adding aria-haspopup="menu" would improve screen reader experience by indicating this button opens a menu.

♿ Suggested improvement
       <button
         onClick={() => setIsOpen((prev) => !prev)}
         aria-label="Toggle profile menu"
         aria-expanded={isOpen}
+        aria-haspopup="menu"
         className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
       >

103-111: Consider attaching outside click listener only when dropdown is open.

The current implementation always listens for mousedown events even when the dropdown is closed. For a single dropdown this is negligible, but you could optimize by conditionally attaching the listener.

♻️ Optional optimization
   useEffect(() => {
+    if (!isOpen) return;
+
     const handler = (e: MouseEvent) => {
       if (!dropdownRef.current?.contains(e.target as Node)) {
         setIsOpen(false);
       }
     };
     document.addEventListener("mousedown", handler);
     return () => document.removeEventListener("mousedown", handler);
-  }, []);
+  }, [isOpen]);

132-146: Consider adding role="menu" for accessibility compliance.

For screen readers to properly announce this as a menu, the dropdown container should have role="menu". This pairs with the aria-haspopup="menu" suggestion on the button.

♿ Suggested improvement
           <motion.div
             initial={{ opacity: 0, y: -10, scale: 0.95 }}
             animate={{ opacity: 1, y: 0, scale: 1 }}
             exit={{ opacity: 0, y: -10, scale: 0.95 }}
             transition={{ duration: 0.2 }}
+            role="menu"
             className="border-border bg-background absolute right-0 z-50 mt-2 min-w-[220px] overflow-hidden rounded-xl border shadow-lg"
           >
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2f890f and 107ea71.

📒 Files selected for processing (1)
  • components/examples/profile-dropdown.tsx
🔇 Additional comments (5)
components/examples/profile-dropdown.tsx (5)

1-12: LGTM!

The imports are well-organized with appropriate separation of React hooks, Next.js components, animation utilities, and custom icons.


14-25: LGTM!

Static items array correctly defined outside the component to avoid recreation on each render. Labels are unique, making them suitable for use as keys.


27-35: LGTM!

The interface properly types the animated icon component with RefAttributes<AnimatedIconHandle> to support the imperative animation control via refs.


37-81: LGTM!

The implementation correctly addresses the double hover animation issue mentioned in the PR:

  • disableHover prop on the Icon prevents the icon's built-in hover animation.
  • Parent Link's mouse events control animation imperatively via ref.
  • The useEffect properly stops animations when isAnimated becomes false.

This aligns with the referenced commit pattern for disabling icon hover when animations are off.


88-151: Well-structured component with proper state management.

The ProfileDropdown correctly:

  • Manages open/close state with keyboard (Escape) and outside click handling.
  • Passes isAnimated prop down to all items, respecting the global animation toggle.
  • Uses Framer Motion's AnimatePresence for smooth enter/exit animations.

This implementation aligns with the PR objective and the commit fix for preventing double hover animation triggers.

@vxnsh1
Copy link
Copy Markdown
Author

vxnsh1 commented Jan 14, 2026

Accepted the incoming change for examples.ts to avoid modifying the global examples registry. My ProfileDropdown component remains in components/examples, so it can be added to the showcase later if needed.

@Abhijit-Jha
Copy link
Copy Markdown
Member

@vxnsh1 i have left some comments

@vxnsh1
Copy link
Copy Markdown
Author

vxnsh1 commented Jan 18, 2026

@vxnsh1 i have left some comments

I’ve addressed the previous review points (ui icons, disableHover, formatting, etc.).
I see remaining bot suggestions about accessibility.
Should I:

  • Just add aria-label + aria-expanded + Escape-to-close
    or
  • Also implement full arrow-key navigation between menu items?

Just want to confirm before pushing another commit.

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