{
  "$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://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json",
    "https://irisui.dev/r/avatar.json"
  ],
  "files": [
    {
      "path": "src/registry/components/avatar-group/avatar-group.tsx",
      "content": "\"use client\";\n\nimport { useState, type CSSProperties } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion, type Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\nimport { Avatar } from \"../avatar/avatar\";\n\nexport interface AvatarGroupMember {\n  name: string;\n  src?: string;\n  status?: \"online\" | \"offline\";\n}\nexport interface AvatarGroupProps {\n  members: AvatarGroupMember[];\n  max?: number;\n  size?: \"sm\" | \"md\" | \"lg\";\n  label?: string;\n}\n\n/** The overflow count rolls the way it moved: more people rise in from below, fewer drop in from above. */\nconst rise: Variants = {\n  hidden: (direction: number) => ({\n    opacity: 0,\n    y: `${0.4 * direction}em`,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n  }),\n  shown: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] },\n  },\n  gone: (direction: number) => ({\n    opacity: 0,\n    y: `${-0.4 * direction}em`,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n    transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n  }),\n};\n// Same keys as `rise` so the settled style is identical whichever branch renders on the server.\nconst fade: Variants = {\n  hidden: { opacity: 0, y: 0, filter: \"blur(0px)\" },\n  shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } },\n  gone: { opacity: 0, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } },\n};\n// A person joining or leaving opens or closes their slot, so the rest of the stack slides instead of jumping.\nconst slot = {\n  initial: { width: 0, opacity: 0, scale: 0.9 },\n  animate: { width: \"auto\", opacity: 1, scale: 1 },\n  exit: { width: 0, opacity: 0, scale: 0.9 },\n};\n\nconst stackSizes = { sm: \"[--avatar-size:28px]\", md: \"[--avatar-size:36px]\", lg: \"[--avatar-size:48px]\" } as const;\nconst chipSizes = { sm: \"size-7 text-[10px]/[inherit]\", md: \"size-9\", lg: \"size-12 text-sm/[inherit]\" } as const;\n/* One wrapper per person carries every hover move as a single translate, so the stack never changes size. Pointing at the stack loosens\n   its overlap from the center; the person under the pointer lifts with a crisp ring and names themselves, and their neighbors ease aside.\n   Reduced motion keeps the stack still; the ring and name still answer the pointer. */\nconst lift =\n  \"relative -ml-1.5 inline-flex rounded-full [--aside:0px] [--fan:0px] [--rise:0px] [transform:translate(calc(var(--fan)_+_var(--aside)),var(--rise))] group-hover/stack:[--fan:calc((var(--index)_-_(var(--count)_-_1)_/_2)_*_4px)] group-hover/slot:[--rise:-2px] [@media(hover:hover)]:[[data-slot]:hover~[data-slot]_&]:[--aside:3px] [@media(hover:hover)]:[[data-slot]:has(~[data-slot]:hover)_&]:[--aside:-3px] motion-reduce:transform-none! motion-reduce:transition-none\";\nconst ring =\n  \"border-2 border-surface shadow-[0_0_0_1px_var(--line)] transition-shadow duration-fast ease-standard group-hover/slot:shadow-[0_0_0_1px_var(--line-strong)]\";\n/* The name floats above the stack, outside the layout, so showing it never moves anything. */\nconst tip =\n  \"pointer-events-none absolute bottom-[calc(100%_+_8px)] left-1/2 z-2 rounded-full bg-ink px-2 py-1 text-xs leading-body font-medium whitespace-nowrap text-page opacity-0 [transform:translate(-50%,3px)] [transition:opacity_var(--duration-fast)_var(--ease-standard),transform_var(--duration-fast)_var(--ease-standard)] group-hover/slot:[transform:translate(-50%,0)] group-hover/slot:opacity-100 group-hover/slot:delay-60 motion-reduce:[transform:translate(-50%,0)] motion-reduce:transition-none\";\n/* Each slot is the avatar's visible width, so a slot that opens or closes moves the rest of the stack smoothly. */\nconst slotClass = \"group/slot relative inline-flex h-(--avatar-size) flex-none items-center hover:z-1\";\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 (\n    <div\n      className={cn(\"group/stack inline-flex h-(--avatar-size) items-center pl-1.5\", stackSizes[size])}\n      role=\"group\"\n      aria-label={label}\n      style={{ \"--count\": visible.length + (overflow > 0 ? 1 : 0) } as CSSProperties}\n    >\n      <AnimatePresence initial={false}>\n        {visible.map((member, index) => (\n          <motion.span\n            key={member.name}\n            data-slot=\"\"\n            className={slotClass}\n            style={{ \"--index\": index } as CSSProperties}\n            {...slot}\n            transition={transition}\n          >\n            <span\n              className={cn(\n                lift,\n                \"[transition:transform_var(--duration-standard)_var(--ease-standard),box-shadow_var(--duration-fast)_var(--ease-standard)]\",\n              )}\n            >\n              <Avatar className={ring} name={member.name} src={member.src} status={member.status} size={size} />\n              <span className={tip} aria-hidden=\"true\">\n                {member.name}\n              </span>\n            </span>\n          </motion.span>\n        ))}\n        {overflow > 0 ? (\n          <motion.span\n            key=\"overflow\"\n            data-slot=\"\"\n            className={slotClass}\n            style={{ \"--index\": visible.length } as CSSProperties}\n            {...slot}\n            transition={transition}\n          >\n            <span\n              className={cn(\n                lift,\n                ring,\n                \"relative inline-grid place-items-center overflow-hidden bg-hover text-xs/[inherit] font-medium text-ink-2 tabular-nums\",\n                chipSizes[size],\n              )}\n              role=\"img\"\n              aria-label={`${overflow} more ${label.toLowerCase()}`}\n            >\n              <AnimatePresence mode=\"popLayout\" initial={false} custom={count.direction}>\n                <motion.span\n                  key={overflow}\n                  className=\"inline-block\"\n                  custom={count.direction}\n                  variants={reduceMotion ? fade : rise}\n                  initial=\"hidden\"\n                  animate=\"shown\"\n                  exit=\"gone\"\n                  aria-hidden=\"true\"\n                >\n                  +{overflow}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default AvatarGroup;\n",
      "type": "registry:component",
      "target": "@components/iris/avatar-group/avatar-group.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/avatar-group",
  "meta": {
    "kind": "component",
    "group": "Data display",
    "export": "AvatarGroup",
    "tags": [
      "identity",
      "people"
    ],
    "docs": "https://irisui.dev/components/avatar-group"
  }
}
