{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "dependencies": [
    "@base-ui/react",
    "@phosphor-icons/react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/avatar.tsx",
      "content": "\"use client\";\n\nimport { Avatar as AvatarPrimitive } from \"@base-ui/react/avatar\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Children, cloneElement, isValidElement } from \"react\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nimport { User } from \"@phosphor-icons/react/dist/ssr\";\nexport const avatarVariants = cva(\n  \"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-ink text-[color:var(--base)]\",\n  {\n    defaultVariants: { size: \"md\", shape: \"circle\" },\n    variants: {\n      size: {\n        xs: \"size-6 text-mini font-medium\",\n        sm: \"size-7 text-mini font-medium\",\n        md: \"size-9 text-small font-medium\",\n        lg: \"size-11 text-small font-medium\",\n        xl: \"size-14 text-regular font-medium\",\n      },\n      shape: {\n        circle: \"rounded-full\",\n        square: \"rounded-[var(--radius-6)]\",\n      },\n    },\n  },\n);\n\ntype AvatarSizeEnum = NonNullable<VariantProps<typeof avatarVariants>[\"size\"]>;\ntype AvatarShape = NonNullable<VariantProps<typeof avatarVariants>[\"shape\"]>;\n/** Size accepts either a preset (`\"xs\"–\"xl\"`) or a pixel number for exact control. */\nexport type AvatarSize = AvatarSizeEnum | number;\n\n/** Map enum sizes to pixel diameter: the source of truth for AvatarGroup overlap math. */\nconst sizePx: Record<AvatarSizeEnum, number> = {\n  xs: 24,\n  sm: 28,\n  md: 36,\n  lg: 44,\n  xl: 56,\n};\n\n/** Resolve any AvatarSize (enum or number) to a pixel value. */\nfunction toPx(size: AvatarSize): number {\n  return typeof size === \"number\" ? size : sizePx[size];\n}\n\n/** Text size (Tailwind class) that reads well inside an avatar of a given\n *  pixel diameter. Monograms carry the medium (500) weight explicitly. */\nfunction textClassForPx(px: number): string {\n  if (px <= 28) return \"text-mini font-medium\";\n  if (px <= 44) return \"text-small font-medium\";\n  return \"text-regular font-medium\";\n}\n\nexport interface AvatarProps\n  extends Omit<React.ComponentProps<typeof AvatarPrimitive.Root>, \"size\"> {\n  size?: AvatarSize;\n  shape?: AvatarShape;\n  /** Shorthand for monogram fallback. Equivalent to `<AvatarFallback>letter</AvatarFallback>`. */\n  letter?: string;\n  /**\n   * When true and no image or letter is provided, renders a generic person icon\n   * on a subtle neutral fill. When combined with `letter`, uses the placeholder\n   * fill instead of the default dark fill.\n   */\n  placeholder?: boolean;\n  /** Render an icon inside the avatar (status/action pattern). Overrides image/letter. */\n  icon?: React.ReactNode;\n  /** When `icon` is set, use the subtle neutral fill. Defaults to true. */\n  iconBackground?: boolean;\n}\n\nexport function Avatar({\n  className,\n  size = \"md\",\n  shape,\n  letter,\n  placeholder,\n  icon,\n  iconBackground = true,\n  style,\n  children,\n  ...props\n}: AvatarProps): React.ReactElement {\n  // Numeric size: apply width/height inline; enum size: use the size variant class.\n  const numericSize = typeof size === \"number\";\n  const px = toPx(size);\n  const inlineSize = numericSize ? { width: size, height: size } : undefined;\n\n  const fillClass = placeholder\n    ? \"bg-fill-1 text-ink-muted\"\n    : \"bg-ink text-[color:var(--base)]\";\n\n  const baseClass = numericSize\n    ? cn(\n        \"relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden\",\n        fillClass,\n        shape === \"square\" ? \"rounded-[var(--radius-6)]\" : \"rounded-full\",\n        textClassForPx(size),\n      )\n    : cn(\n        avatarVariants({ size, shape }),\n        placeholder && \"!bg-fill-1 !text-ink-muted\",\n      );\n\n  // Main avatar body: children > letter > placeholder icon.\n  const body =\n    children ??\n    (letter ? (\n      <AvatarFallback>{letter}</AvatarFallback>\n    ) : placeholder ? (\n      <PlaceholderIcon />\n    ) : null);\n\n  // Badge overlay sits in the bottom-left corner and is roughly 45% of avatar size.\n  // Uses `overflow-visible` on the wrapper so the badge can extend past the avatar edge.\n  if (icon) {\n    const badgePx = Math.max(14, Math.round(px * 0.45));\n    return (\n      <span\n        className={cn(\"relative inline-flex shrink-0\", inlineSize && \"\")}\n        style={inlineSize}\n      >\n        <AvatarPrimitive.Root\n          data-slot=\"avatar\"\n          className={cn(baseClass, className)}\n          style={{ ...inlineSize, ...style }}\n          {...props}\n        >\n          {body}\n        </AvatarPrimitive.Root>\n        <span\n          data-slot=\"avatar-badge\"\n          aria-hidden=\"true\"\n          className={cn(\n            \"absolute -bottom-0.5 -left-0.5 inline-flex items-center justify-center rounded-full\",\n            \"ring-2 ring-base\",\n            iconBackground\n              ? \"bg-ink text-[color:var(--base)]\"\n              : \"bg-base text-ink-muted\",\n          )}\n          style={{ width: badgePx, height: badgePx }}\n        >\n          {icon}\n        </span>\n      </span>\n    );\n  }\n\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(baseClass, className)}\n      style={{ ...inlineSize, ...style }}\n      {...props}\n    >\n      {body}\n    </AvatarPrimitive.Root>\n  );\n}\n\nfunction PlaceholderIcon(): React.ReactElement {\n  return <User aria-hidden className=\"size-[60%]\" />;\n}\n\nexport function AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>): React.ReactElement {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"size-full object-cover\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>): React.ReactElement {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\"flex size-full items-center justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\n/* --------------------------- AvatarGroup --------------------------- */\n\n/** A member entry for the `members` prop. Provide `image`, `letter`, or both. */\nexport interface AvatarGroupMember {\n  /** Image URL. If omitted, `letter` renders as a fallback monogram. */\n  image?: string;\n  /** 1–2 character monogram fallback. */\n  letter?: string;\n  /** Alt text for the image. Defaults to `letter` or \"Avatar\". */\n  alt?: string;\n  /** Optional stable key. */\n  id?: string | number;\n}\n\nexport interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Data-driven mode: pass members as an array. Alternative to nested `<Avatar>` children. */\n  members?: AvatarGroupMember[];\n  /** Maximum avatars before collapsing to a `+N` chip. 0 disables the cap. Defaults to 5. */\n  limit?: number;\n  /**\n   * Overlap between avatars. `\"auto\"` scales with size (roughly a third of the avatar);\n   * a number sets exact pixel overlap. Defaults to `\"auto\"`.\n   */\n  overlap?: \"auto\" | number;\n  /** Reverse the z-stacking so the last member sits on top. Left-to-right order is unchanged. */\n  reverse?: boolean;\n  /** Size applied to every child and the overflow chip. Accepts enum or number. */\n  size?: AvatarSize;\n  /** Shape applied to every child and the overflow chip. */\n  shape?: AvatarShape;\n  /** @deprecated Prefer `limit`. Retained for backwards compatibility. */\n  max?: number;\n}\n\n/** Auto-overlap heuristic: ~30% of the avatar diameter, capped for very tiny/large sizes. */\nfunction autoOverlapPx(size: AvatarSize): number {\n  const px = toPx(size);\n  return Math.max(4, Math.round(px * 0.3));\n}\n\nexport function AvatarGroup({\n  className,\n  members,\n  limit,\n  max,\n  overlap = \"auto\",\n  reverse = false,\n  size = \"md\",\n  shape = \"circle\",\n  children,\n  style,\n  ...props\n}: AvatarGroupProps): React.ReactElement {\n  const cap = limit ?? max ?? 5;\n  const overlapPx = overlap === \"auto\" ? autoOverlapPx(size) : overlap;\n  const marginStart = -overlapPx;\n  const ring = ringWidthForSize(size);\n\n  // Data-driven path (members array).\n  if (members) {\n    const showChip = cap > 0 && members.length > cap;\n    const shown = showChip ? members.slice(0, cap) : members;\n    const overflow = showChip ? members.length - cap : 0;\n\n    return (\n      <div\n        data-slot=\"avatar-group\"\n        className={cn(\"inline-flex items-center\", className)}\n        style={style}\n        {...props}\n      >\n        {shown.map((m, i) => (\n          <Avatar\n            key={m.id ?? i}\n            size={size}\n            shape={shape}\n            className={cn(ring, \"ring-base\")}\n            style={{\n              marginInlineStart: i === 0 ? 0 : marginStart,\n              zIndex: reverse ? i : shown.length - i,\n            }}\n          >\n            {m.image && <AvatarImage src={m.image} alt={m.alt ?? m.letter ?? \"Avatar\"} />}\n            <AvatarFallback>{m.letter ?? \"\"}</AvatarFallback>\n          </Avatar>\n        ))}\n        {showChip && (\n          <span\n            data-slot=\"avatar-group-overflow\"\n            className={cn(\"ms-2\", overflowTextClass(size))}\n          >\n            +{overflow}\n          </span>\n        )}\n      </div>\n    );\n  }\n\n  // Children path (backwards-compat with the original API).\n  const all = Children.toArray(children).filter(isValidElement);\n  const showChip = cap > 0 && all.length > cap;\n  const shown = showChip ? all.slice(0, cap) : all;\n  const overflow = showChip ? all.length - cap : 0;\n\n  const decorated = shown.map((child, i) => {\n    const childProps = (child as React.ReactElement<AvatarProps>).props;\n    return cloneElement(child as React.ReactElement<AvatarProps>, {\n      size: childProps.size ?? size,\n      shape: childProps.shape ?? shape,\n      className: cn(ring, \"ring-base\", childProps.className),\n      style: {\n        marginInlineStart: i === 0 ? 0 : marginStart,\n        zIndex: reverse ? i : shown.length - i,\n        ...childProps.style,\n      },\n    });\n  });\n\n  return (\n    <div\n      data-slot=\"avatar-group\"\n      className={cn(\"inline-flex items-center\", className)}\n      style={style}\n      {...props}\n    >\n      {decorated}\n      {showChip && (\n        <span\n          data-slot=\"avatar-group-overflow\"\n          className={cn(\"ms-2\", overflowTextClass(size))}\n        >\n          +{overflow}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/** Text sizing for the \"+N\" overflow indicator. Reads a step smaller\n *  than the initials inside an avatar so it feels like a quiet counter\n *  rather than another face. */\nfunction overflowTextClass(size: AvatarSize): string {\n  const px = toPx(size);\n  const cls = px <= 32 ? \"text-mini\" : px <= 44 ? \"text-small\" : \"text-small\";\n  return cn(\"shrink-0 select-none text-ink-muted\", cls);\n}\n\nfunction ringWidthForSize(size: AvatarSize): string {\n  const px = toPx(size);\n  if (px <= 20) return \"ring-[1.5px]\";\n  if (px <= 32) return \"ring-2\";\n  return \"ring-[2.5px]\";\n}\n\nexport { AvatarPrimitive };\n",
      "type": "registry:ui",
      "target": "components/ui/avatar.tsx"
    }
  ],
  "type": "registry:ui"
}