{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"avatar","type":"registry:ui","title":"Avatar","description":"Profile picture that fades in once loaded, falls back to colorful initials, and shows an optional presence dot.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/avatar.tsx","type":"registry:ui","content":"\"use client\";\n/* eslint-disable @next/next/no-img-element */\n\nimport { useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AvatarStatus = \"online\" | \"away\" | \"busy\" | \"offline\";\n\nexport interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {\n  src?: string;\n  /** Person's name: used for the accessible label and the fallback initials. */\n  name?: string;\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n  shape?: \"circle\" | \"square\";\n  status?: AvatarStatus;\n}\n\nconst sizes = {\n  sm: { box: \"size-8 text-xs\", dot: \"size-2.5\" },\n  md: { box: \"size-10 text-sm\", dot: \"size-3\" },\n  lg: { box: \"size-14 text-base\", dot: \"size-3.5\" },\n  xl: { box: \"size-20 text-xl\", dot: \"size-4.5\" },\n};\n\nconst dots: Record<AvatarStatus, string> = {\n  online: \"bg-emerald-500\",\n  away: \"bg-amber-400\",\n  busy: \"bg-red-500\",\n  offline: \"bg-zinc-400\",\n};\n\nfunction initials(name?: string) {\n  const parts = (name ?? \"\").trim().split(/\\s+/).filter(Boolean);\n  if (!parts.length) return \"?\";\n  return ((parts[0][0] ?? \"\") + (parts.length > 1 ? parts[parts.length - 1][0] : \"\")).toUpperCase();\n}\n\n/** Stable hue per name so fallbacks are colorful but deterministic (no hydration mismatch). */\nfunction hue(name = \"\") {\n  let h = 0;\n  for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;\n  return h;\n}\n\nexport function Avatar({ src, name, size = \"md\", shape = \"circle\", status, className, ...props }: AvatarProps) {\n  const [loadedSrc, setLoadedSrc] = useState<string>();\n  const [failedSrc, setFailedSrc] = useState<string>();\n  const loaded = !!src && loadedSrc === src;\n  const showImage = src && failedSrc !== src;\n  const radius = shape === \"circle\" ? \"rounded-full\" : \"rounded-[28%]\";\n  const h = hue(name);\n\n  return (\n    <span\n      role=\"img\"\n      aria-label={[name ?? \"Avatar\", status].filter(Boolean).join(\", \")}\n      {...props}\n      className={cn(\"relative inline-flex shrink-0 select-none\", sizes[size].box, className)}\n    >\n      <span\n        aria-hidden\n        className={cn(\"grid size-full place-items-center overflow-hidden font-semibold ring-2 ring-background\", radius)}\n        style={{\n          background: `linear-gradient(135deg, oklch(0.78 0.11 ${h}), oklch(0.62 0.14 ${(h + 40) % 360}))`,\n          color: \"oklch(0.22 0.05 \" + h + \")\",\n        }}\n      >\n        <span\n          className={cn(\n            \"col-start-1 row-start-1 transition-opacity duration-300\",\n            showImage && loaded && \"opacity-0\",\n          )}\n        >\n          {initials(name)}\n        </span>\n        {showImage && (\n          <img\n            src={src}\n            alt=\"\"\n            draggable={false}\n            // Cached images can finish before hydration attaches onLoad.\n            ref={(img) => {\n              if (img?.complete && img.naturalWidth) setLoadedSrc(src);\n            }}\n            onLoad={() => setLoadedSrc(src)}\n            onError={() => setFailedSrc(src)}\n            className={cn(\n              \"col-start-1 row-start-1 size-full object-cover transition-[opacity,scale] duration-500 ease-out motion-reduce:transition-none\",\n              loaded ? \"scale-100 opacity-100\" : \"scale-110 opacity-0\",\n            )}\n          />\n        )}\n      </span>\n      {status && (\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute bottom-0 right-0 rounded-full ring-2 ring-background\",\n            shape === \"square\" && \"-bottom-0.5 -right-0.5\",\n            sizes[size].dot,\n            dots[status],\n          )}\n        >\n          {status === \"online\" && (\n            <span className=\"absolute inset-0 animate-ping rounded-full bg-emerald-500 opacity-60 motion-reduce:hidden\" />\n          )}\n        </span>\n      )}\n    </span>\n  );\n}\n"}]}