{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "avatar-hover-card", "type": "registry:ui", "title": "Avatar Hover Card", "description": "An interactive avatar that reveals a profile card on hover with spring physics, staggered content, and delayed open/close.", "dependencies": [ "motion" ], "registryDependencies": [ "avatar" ], "files": [ { "path": "registry/ruixenui/avatar-hover-card.tsx", "content": "\"use client\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { type ReactNode, useId, useRef, useState } from \"react\";\n\nexport interface AvatarHoverCardProps {\n imageSrc: string;\n imageAlt?: string;\n name: string;\n username?: string;\n description?: string;\n stats?: { label: string; value: string }[];\n actions?: ReactNode;\n size?: \"sm\" | \"md\" | \"lg\";\n align?: \"start\" | \"center\" | \"end\";\n className?: string;\n}\n\nconst triggerSizes = {\n sm: \"size-8\",\n md: \"size-10\",\n lg: \"size-12\",\n};\n\nfunction initials(name: string) {\n return name\n .split(\" \")\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2);\n}\n\nexport default function AvatarHoverCard({\n imageSrc,\n imageAlt,\n name,\n username,\n description,\n stats,\n actions,\n size = \"md\",\n align = \"start\",\n className,\n}: AvatarHoverCardProps) {\n const [open, setOpen] = useState(false);\n const timer = useRef | null>(null);\n const uid = useId();\n\n const show = () => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => setOpen(true), 80);\n };\n\n const hide = () => {\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => setOpen(false), 100);\n };\n\n const alignClass: Record = {\n start: \"left-0\",\n center: \"left-1/2 -translate-x-1/2\",\n end: \"right-0\",\n };\n\n const spring = { type: \"spring\" as const, stiffness: 500, damping: 35 };\n\n const avatarContent = (\n \n \n \n {initials(name)}\n \n \n );\n\n return (\n \n {/* Trigger — always in flow for layout stability */}\n \n {!open && (\n \n {avatarContent}\n \n )}\n \n\n {/* Card */}\n \n {open && (\n \n
\n {/* Header — avatar morphs here via layoutId */}\n
\n \n {avatarContent}\n \n \n

\n {name}\n

\n {username && (\n

\n @{username}\n

\n )}\n \n
\n\n {/* Description */}\n {description && (\n \n {description}\n \n )}\n\n {/* Stats */}\n {stats && stats.length > 0 && (\n \n {stats.map((s) => (\n \n \n {s.value}\n {\" \"}\n {s.label}\n \n ))}\n \n )}\n\n {/* Actions */}\n {actions && (\n \n {actions}\n \n )}\n
\n \n )}\n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/avatar-hover-card.tsx" } ] }