{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "avatar-spring-stack", "type": "registry:ui", "title": "Avatar Spring Stack", "description": "An overlapping avatar stack with spring-animated tooltips that reveal labels on hover, plus a +N overflow counter.", "dependencies": [ "motion" ], "registryDependencies": [ "avatar" ], "files": [ { "path": "registry/ruixenui/avatar-spring-stack.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 { useState } from \"react\";\n\n/* ═══════════════════════════════════════════════════════════\n Magnetic Repulsion — each avatar is a charged body. Hover\n one and neighbors spring away in proportion to 1/distance,\n creating a lens-like spread that follows your cursor.\n The wave isn't animated — it emerges from the spring physics.\n ═══════════════════════════════════════════════════════════ */\n\ninterface AvatarSpringStackItem {\n src: string;\n alt?: string;\n label?: string;\n}\n\ninterface AvatarSpringStackProps {\n avatars: AvatarSpringStackItem[];\n maxVisible?: number;\n size?: number;\n className?: string;\n}\n\nexport default function AvatarSpringStack({\n avatars,\n maxVisible = 5,\n size = 44,\n className,\n}: AvatarSpringStackProps) {\n const [activeIdx, setActiveIdx] = useState(null);\n\n const visible = avatars.slice(0, maxVisible);\n const extra = avatars.length - maxVisible;\n const total = visible.length + (extra > 0 ? 1 : 0);\n\n const overlap = Math.round(size * 0.32);\n const step = size - overlap;\n const spread = Math.round(size * 0.45);\n\n function getOffset(idx: number) {\n if (activeIdx === null) return 0;\n const d = idx - activeIdx;\n if (d === 0) return 0;\n return (d > 0 ? 1 : -1) * (spread / Math.abs(d));\n }\n\n const hasActive = activeIdx !== null;\n\n return (\n setActiveIdx(null)}\n >\n {visible.map((avatar, idx) => {\n const active = activeIdx === idx;\n\n return (\n setActiveIdx(idx)}\n >\n \n \n \n \n {avatar.label?.[0]?.toUpperCase() || \"?\"}\n \n \n \n\n \n {active && avatar.label && (\n \n {avatar.label}\n \n )}\n \n \n );\n })}\n\n {extra > 0 && (\n \n \n +{extra}\n \n \n )}\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/avatar-spring-stack.tsx" } ] }