{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "title": "Avatar",
  "description": "Avatar and AvatarGroup on Base UI Avatar: images fade in from a 4px blur once loaded so initials never flash; overlapping stacks with a cut-out ring spread apart on hover; the overflow count rolls.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-number-roll.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/avatar/avatar.tsx",
      "content": "\"use client\";\n\nimport { Avatar as BaseAvatar } from \"@base-ui/react/avatar\";\nimport { Children, isValidElement, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { NumberRoll } from \"../_primitives/number-roll/number-roll\";\nimport { duration } from \"../lib/motion\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nexport type AvatarProps = {\n  /** The person's name: the accessible name and the source of the fallback initials. */\n  name: string;\n  /** Image URL. While it loads the avatar shows a quiet tile; initials appear only if it fails or is missing. */\n  src?: string;\n  /** Override the initials, e.g. for a team or a bot. Defaults to the first and last name's initials. */\n  initials?: string;\n  /** 20, 24, 32 (default), 40 or 48px. */\n  size?: AvatarSize;\n  /** `circle` (default) or `square`, which takes the control radius for its size; 48px squares get a superellipse. */\n  shape?: \"circle\" | \"square\";\n  className?: string;\n};\n\nconst SIZES: Record<AvatarSize, { box: string; text: string; square: string; overlap: string }> = {\n  xs: { box: \"size-5\", text: \"text-ui-2xs\", square: \"rounded-control-2xs\", overlap: \"-ml-1\" },\n  sm: { box: \"size-6\", text: \"text-ui-2xs\", square: \"rounded-control-xs\", overlap: \"-ml-1.5\" },\n  md: { box: \"size-8\", text: \"text-ui-xs\", square: \"rounded-control-sm\", overlap: \"-ml-2\" },\n  lg: { box: \"size-10\", text: \"text-ui\", square: \"rounded-control\", overlap: \"-ml-2.5\" },\n  xl: { box: \"size-12\", text: \"text-ui-md\", square: \"rounded-panel squircle\", overlap: \"-ml-3\" },\n};\n\n/** \"Maya Okafor\" → \"MO\"; \"maya\" → \"M\". 20px avatars keep one letter. */\nexport function initialsOf(name: string, max = 2) {\n  const words = name.trim().split(/\\s+/).filter(Boolean);\n  const picked = words.length > 1 ? [words[0], words[words.length - 1]] : words;\n  return picked\n    .slice(0, max)\n    .map((w) => [...w][0]?.toLocaleUpperCase() ?? \"\")\n    .join(\"\");\n}\n\n/** How long a loading image may keep the tile blank before the initials show anyway, ms. */\nconst FALLBACK_DELAY = duration.fallback * 1000;\n\n/**\n * A profile picture on Base UI's Avatar. The image fades in from a 4px blur once it has\n * loaded, so a fast image never flashes the initials first; a slow, failed or missing image\n * falls back to initials. Every avatar wears a 1px alpha ring so photos keep an edge on any\n * background.\n */\nexport function Avatar({ name, src, initials, size = \"md\", shape = \"circle\", className = \"\" }: AvatarProps) {\n  const s = SIZES[size];\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"loaded\" | \"error\">(\"idle\");\n  // No image or a failed one: initials at once. Loading: wait, so a quick image arrives first.\n  const delay = src && status !== \"error\" ? FALLBACK_DELAY : 0;\n  const letters = initials ?? initialsOf(name, size === \"xs\" ? 1 : 2);\n\n  return (\n    <BaseAvatar.Root\n      role=\"img\"\n      aria-label={name}\n      className={`image-ring inline-flex shrink-0 items-center justify-center overflow-hidden bg-surface-active align-middle font-medium text-text-secondary select-none ${s.box} ${\n        shape === \"circle\" ? \"rounded-pill\" : s.square\n      } ${className}`}\n    >\n      {src && (\n        <BaseAvatar.Image\n          src={src}\n          alt=\"\"\n          onLoadingStatusChange={setStatus}\n          className=\"size-full object-cover transition-[opacity,filter] duration-(--eased-duration-standard) ease-enter data-starting-style:opacity-0 data-starting-style:blur-[4px] reduced:duration-(--eased-duration-instant) reduced:data-starting-style:blur-none\"\n        />\n      )}\n      <BaseAvatar.Fallback\n        delay={delay}\n        aria-hidden\n        className={`leading-none transition-opacity duration-(--eased-duration-fast) ease-standard starting:opacity-0 ${s.text}`}\n      >\n        {letters}\n      </BaseAvatar.Fallback>\n    </BaseAvatar.Root>\n  );\n}\n\nexport type AvatarGroupProps = {\n  /** `Avatar` elements. Their `size` is set by the group. */\n  children: ReactNode;\n  /** Show at most this many; the rest collapse into a rolling \"+N\" tile. */\n  max?: number;\n  size?: AvatarSize;\n  shape?: \"circle\" | \"square\";\n  /** The group's accessible name, e.g. \"Design team\". */\n  \"aria-label\"?: string;\n  /**\n   * The colour of the cut-out ring that separates overlapping avatars; match the surface the\n   * group sits on. Default `var(--eased-surface)`.\n   */\n  cutout?: string;\n  className?: string;\n};\n\n/**\n * Overlapping avatars with a cut-out ring between them. On a fine pointer (or when focus is\n * inside) the stack spreads apart on a spring so each face can be seen; the overflow count rolls.\n */\nexport function AvatarGroup({\n  children,\n  max,\n  size = \"md\",\n  shape = \"circle\",\n  \"aria-label\": ariaLabel,\n  cutout = \"var(--eased-surface)\",\n  className = \"\",\n}: AvatarGroupProps) {\n  const s = SIZES[size];\n  const items = Children.toArray(children).filter(isValidElement<AvatarProps>);\n  const shown = max !== undefined && items.length > max ? items.slice(0, Math.max(0, max - 1)) : items;\n  const rest = items.length - shown.length;\n  const radius = shape === \"circle\" ? \"rounded-pill\" : s.square;\n  const spread =\n    \"transition-[translate] duration-(--eased-duration-spring-snappy) ease-spring-snappy reduced:transition-none\";\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={ariaLabel}\n      style={{ \"--avatar-cutout\": cutout } as CSSProperties}\n      className={`isolate flex items-center hover-fine:[&>*]:translate-x-[calc(var(--i)*0.25rem)] focus-within:[&>*]:translate-x-[calc(var(--i)*0.25rem)] reduced:[&>*]:translate-x-0! ${className}`}\n    >\n      {shown.map((item, i) => (\n        <span\n          key={item.key ?? i}\n          style={{ \"--i\": i, zIndex: shown.length - i } as CSSProperties}\n          className={`${i > 0 ? s.overlap : \"\"} inline-flex ${radius} ring-2 ring-(--avatar-cutout) ${spread}`}\n        >\n          <Avatar {...item.props} size={size} shape={shape} />\n        </span>\n      ))}\n      {rest > 0 && (\n        <span\n          role=\"img\"\n          aria-label={`and ${rest} more`}\n          style={{ \"--i\": shown.length } as CSSProperties}\n          className={`${shown.length > 0 ? s.overlap : \"\"} inline-flex ${s.box} ${radius} items-center justify-center bg-surface-hover font-medium text-text-secondary ring-2 ring-(--avatar-cutout) ${s.text} ${spread}`}\n        >\n          <span aria-hidden className=\"inline-flex leading-none\">\n            +<NumberRoll value={rest} aria-hidden />\n          </span>\n        </span>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/avatar/avatar.tsx"
    }
  ],
  "docs": "Import Avatar and AvatarGroup from components/eased/avatar/avatar. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}