{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"avatar-group","type":"registry:ui","title":"Avatar group","description":"Show a team or set of contributors in a small space.","categories":["data"],"dependencies":["motion"],"registryDependencies":["https://uiarc.dev/r/arc-motion-tokens.json","https://uiarc.dev/r/avatar.json"],"files":[{"path":"registry/components/avatar-group/avatar-group.tsx","type":"registry:component","target":"@components/arc/avatar-group/avatar-group.tsx","content":"\"use client\";\n\nimport { useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion, type Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { Avatar } from \"../avatar/avatar\";\nimport styles from \"./avatar-group.module.css\";\n\nexport interface AvatarGroupMember { name: string; src?: string; status?: \"online\" | \"offline\"; }\nexport interface AvatarGroupProps { members: AvatarGroupMember[]; max?: number; size?: \"sm\" | \"md\" | \"lg\"; label?: string; }\n\n/** The overflow count rolls the way it moved: more people rise in from below, fewer drop in from above. */\nconst rise: Variants = { hidden: (direction: number) => ({ opacity: 0, y: `${.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)` }), shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: (direction: number) => ({ opacity: 0, y: `${-.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }) };\n// Same keys as `rise` so the settled style is identical whichever branch renders on the server.\nconst fade: Variants = { hidden: { opacity: 0, y: 0, filter: \"blur(0px)\" }, shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } } };\n// A person joining or leaving opens or closes their slot, so the rest of the stack slides instead of jumping.\nconst slot = { initial: { width: 0, opacity: 0, scale: .9 }, animate: { width: \"auto\", opacity: 1, scale: 1 }, exit: { width: 0, opacity: 0, scale: .9 } };\n\nexport function AvatarGroup({ members, max = 4, size = \"md\", label = \"Team members\" }: AvatarGroupProps) {\n  const reduceMotion = !!useReducedMotion();\n  const visible = members.slice(0, Math.max(0, max));\n  const overflow = Math.max(0, members.length - visible.length);\n  const transition = reduceMotion ? { duration: 0 } : motionTokens.spring.morph;\n  const [count, setCount] = useState({ overflow, direction: 1 });\n  if (count.overflow !== overflow) setCount({ overflow, direction: overflow < count.overflow ? -1 : 1 });\n  return <div className={[styles.group, styles[size]].join(\" \")} role=\"group\" aria-label={label}>\n    <AnimatePresence initial={false}>\n      {visible.map((member) => <motion.span key={member.name} className={styles.slot} {...slot} transition={transition}><span className={styles.lift}><Avatar className={styles.avatar} name={member.name} src={member.src} status={member.status} size={size} /><span className={styles.tip} aria-hidden=\"true\">{member.name}</span></span></motion.span>)}\n      {overflow > 0 ? <motion.span key=\"overflow\" className={styles.slot} {...slot} transition={transition}><span className={[styles.lift, styles.overflow, styles[size]].join(\" \")} role=\"img\" aria-label={`${overflow} more ${label.toLowerCase()}`}><AnimatePresence mode=\"popLayout\" initial={false} custom={count.direction}><motion.span key={overflow} className={styles.count} custom={count.direction} variants={reduceMotion ? fade : rise} initial=\"hidden\" animate=\"shown\" exit=\"gone\" aria-hidden=\"true\">+{overflow}</motion.span></AnimatePresence></span></motion.span> : null}\n    </AnimatePresence>\n  </div>;\n}\n\nexport default AvatarGroup;\n"},{"path":"registry/components/avatar-group/avatar-group.module.css","type":"registry:component","target":"@components/arc/avatar-group/avatar-group.module.css","content":".group { --avatar-size: 36px; display: inline-flex; height: var(--avatar-size); align-items: center; padding-left: 6px; }\n.group.sm { --avatar-size: 28px; }\n.group.lg { --avatar-size: 48px; }\n/* Each slot is the avatar's visible width, so a slot that opens or closes moves the rest of the stack smoothly. */\n.slot { position: relative; display: inline-flex; flex: 0 0 auto; height: var(--avatar-size); align-items: center; }\n/* One wrapper per person holds the overlap. Avatars never move on hover or focus; only the ring and name answer the pointer. */\n.lift { position: relative; display: inline-flex; margin-left: -6px; border-radius: var(--radius-pill); }\n.avatar, .overflow { border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); transition: box-shadow var(--duration-fast) var(--ease-standard); }\n.overflow { position: relative; display: inline-grid; place-items: center; overflow: hidden; border-radius: var(--radius-pill); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; }\n.count { display: inline-block; }\n.overflow.sm { width: 28px; height: 28px; font-size: 10px; }\n.overflow.md { width: 36px; height: 36px; }\n.overflow.lg { width: 48px; height: 48px; font-size: var(--text-sm); }\n/* The name floats above the stack, outside the layout, so showing it never moves anything. */\n.tip { position: absolute; z-index: 2; bottom: calc(100% + 8px); left: 50%; padding: 4px 8px; border-radius: var(--radius-pill); background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }\n/* The person under the pointer comes to the front with a crisp ring and names themselves; nothing in the stack moves. */\n@media (hover: hover) and (pointer: fine) {\n  .slot:hover { z-index: 1; }\n  .slot:hover .avatar, .slot:hover .overflow { box-shadow: 0 0 0 1px var(--border-strong); }\n  .slot:hover .tip { opacity: 1; transform: translate(-50%, 0); transition-delay: 60ms; }\n}\n/* Reduced motion shows the name without its small rise. */\n@media (prefers-reduced-motion: reduce) { .tip { transform: translate(-50%, 0); transition: none; } }\n"}],"docs":"Docs and live preview: https://uiarc.dev/components/avatar-group","meta":{"tier":"free","kind":"component","tags":["identity","people"],"docs":"https://uiarc.dev/components/avatar-group","markdown":"https://uiarc.dev/components/avatar-group/markdown"}}