{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A Radix avatar with image and initials fallback, three sizes, an optional presence dot, and AvatarGroup for overlapping stacks with a +N overflow counter.",
  "dependencies": [
    "radix-ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/avatar.tsx",
      "content": "// Ballmac UI: Avatar. https://ui.ballmac.com/components/avatar\n// Based on shadcn/ui's Avatar (MIT, Copyright (c) 2023 shadcn), restyled with sizes, a status indicator and AvatarGroup.\n\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Avatar as AvatarPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages, defineMessage } from \"@/lib/ballmac/i18n\"\n\nconst avatarVariants = cva(\n  \"relative flex shrink-0 select-none rounded-full bg-muted align-middle [&_[data-slot=avatar-fallback]]:font-medium\",\n  {\n    variants: {\n      size: {\n        sm: \"size-6 text-[10px]\",\n        default: \"size-8 text-xs\",\n        lg: \"size-11 text-sm\",\n      },\n    },\n    defaultVariants: { size: \"default\" },\n  }\n)\n\ntype AvatarSize = NonNullable<VariantProps<typeof avatarVariants>[\"size\"]>\n\nconst AvatarGroupContext = React.createContext<{ size?: AvatarSize } | null>(null)\n\nconst statusStyles = {\n  online: { className: \"bg-chart-2\", label: defineMessage(\"avatar.statusStyles.online\", \"Online\") },\n  away: { className: \"bg-chart-3\", label: defineMessage(\"avatar.statusStyles.away\", \"Away\") },\n  busy: { className: \"bg-destructive\", label: defineMessage(\"avatar.statusStyles.busy\", \"Busy\") },\n  offline: { className: \"bg-muted-foreground\", label: defineMessage(\"avatar.statusStyles.offline\", \"Offline\") },\n} as const\n\ntype AvatarStatus = keyof typeof statusStyles\n\ntype AvatarProps = React.ComponentProps<typeof AvatarPrimitive.Root> &\n  VariantProps<typeof avatarVariants> & {\n    /** Presence dot in the bottom-right corner. Announced to screen readers as text. */\n    status?: AvatarStatus\n    /** Overrides the screen reader text for the status (defaults to \"Online\", \"Away\", …). */\n    statusLabel?: string\n  }\n\nfunction Avatar({ className, size, status, statusLabel, children, ...props }: AvatarProps) {\n  const msg = useMessages()\n  const group = React.useContext(AvatarGroupContext)\n  const resolvedSize = size ?? group?.size ?? \"default\"\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      data-size={resolvedSize}\n      className={cn(avatarVariants({ size: resolvedSize }), group && \"ring-2 ring-background\", className)}\n      {...props}\n    >\n      {children}\n      {status ? (\n        <span\n          data-slot=\"avatar-status\"\n          data-status={status}\n          className={cn(\n            \"absolute end-0 bottom-0 block rounded-full ring-2 ring-background\",\n            resolvedSize === \"sm\" ? \"size-1.5\" : resolvedSize === \"lg\" ? \"size-3\" : \"size-2.5\",\n            statusStyles[status].className\n          )}\n        >\n          <span className=\"sr-only\">{statusLabel ?? msg.of(statusStyles[status].label)}</span>\n        </span>\n      ) : null}\n    </AvatarPrimitive.Root>\n  )\n}\n\nfunction AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full rounded-full object-cover\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction AvatarFallback({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"flex size-full items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground uppercase [&_svg:not([class*='size-'])]:size-[55%]\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype AvatarGroupProps = React.ComponentProps<\"div\"> & {\n  /** Size applied to every avatar in the group (an avatar's own size prop still wins). */\n  size?: AvatarSize\n  /** Show at most this many avatars, then a \"+N\" counter for the rest. */\n  max?: number\n  /** Total count when it's larger than the avatars you render (for example 128 members, 5 fetched). */\n  total?: number\n}\n\nfunction AvatarGroup({ className, size = \"default\", max, total, children, ...props }: AvatarGroupProps) {\n  const msg = useMessages()\n  const items = React.Children.toArray(children).filter(React.isValidElement)\n  const visible = max !== undefined && max >= 0 ? items.slice(0, max) : items\n  const hidden = Math.max((total ?? items.length) - visible.length, 0)\n  const context = React.useMemo(() => ({ size }), [size])\n\n  return (\n    <AvatarGroupContext.Provider value={context}>\n      <div\n        data-slot=\"avatar-group\"\n        role=\"group\"\n        className={cn(\n          \"flex items-center\",\n          size === \"sm\" ? \"-space-x-1.5\" : size === \"lg\" ? \"-space-x-3\" : \"-space-x-2\",\n          className\n        )}\n        {...props}\n      >\n        {visible}\n        {hidden > 0 ? (\n          <span\n            data-slot=\"avatar-group-count\"\n            className={cn(\n              avatarVariants({ size }),\n              \"items-center justify-center font-medium text-muted-foreground tabular-nums ring-2 ring-background\"\n            )}\n          >\n            <span aria-hidden=\"true\">+{hidden}</span>\n            <span className=\"sr-only\">{msg(\"avatar.more\", \"{count} more\", { count: hidden })}</span>\n          </span>\n        ) : null}\n      </div>\n    </AvatarGroupContext.Provider>\n  )\n}\n\nexport { Avatar, AvatarFallback, AvatarGroup, AvatarImage, avatarVariants, type AvatarGroupProps, type AvatarProps, type AvatarStatus }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/avatar.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "avatar",
      "user",
      "profile",
      "presence",
      "radix"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "User or team picture with initials fallback while the image loads or if it fails. Add status for presence, and wrap several in AvatarGroup with max to cap the visible count.",
      "whenToUse": [
        "Showing who owns, edited or is assigned to something",
        "Account menus and comment threads",
        "Stacks of collaborators or members with an overflow count"
      ],
      "whenNotToUse": [
        "Logos or product icons (use an img)",
        "Large profile headers that need a custom shape"
      ],
      "composesWith": [
        "badge",
        "tooltip",
        "dialog"
      ],
      "a11y": [],
      "customization": [
        "size: sm | default | lg (24 / 32 / 44 px)",
        "status: online | away | busy | offline, announced as screen reader text (statusLabel to override)",
        "AvatarGroup: size, max, total (when more members exist than you render)",
        "Always include an AvatarFallback with initials; the image alt names the person"
      ]
    },
    "source": {
      "name": "shadcn/ui Avatar",
      "url": "https://github.com/shadcn-ui/ui",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 shadcn",
      "modified": true
    },
    "examples": [
      "avatar-demo",
      "avatar-group"
    ],
    "url": "https://ui.ballmac.com/components/avatar"
  },
  "categories": [
    "primitives"
  ],
  "type": "registry:ui"
}