{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "avatar",
  "type": "registry:ui",
  "title": "Avatar",
  "description": "Stable identity avatar primitive with native image behavior, deterministic initials and icon fallbacks, accessible decorative modes, constrained tones, rings, shapes, and optional reduced-motion-aware hover polish.",
  "dependencies": [
    "motion@^12.43.0"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/avatar/avatar.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/avatar/avatar.tsx",
      "content": "import {\n  forwardRef,\n  useState,\n  type FocusEventHandler,\n  type HTMLAttributes,\n  type ImgHTMLAttributes,\n  type MouseEventHandler,\n  type PointerEventHandler,\n  type ReactNode,\n} from \"react\";\nimport {\n  motion as motionElement,\n  useReducedMotion,\n  type HTMLMotionProps,\n  type TargetAndTransition,\n} from \"motion/react\";\nimport { cn } from \"../../utils/cn\";\nimport { useHydrated } from \"../../utils/use-hydrated\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\";\nexport type AvatarShape = \"circle\" | \"rounded\" | \"square\";\nexport type AvatarTone =\n  \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"destructive\" | \"info\";\nexport type AvatarRing = \"none\" | \"border\" | \"ring\";\nexport type AvatarMotion = \"none\" | \"subtle\" | \"standard\";\nexport type AvatarImageState = \"image\" | \"fallback\" | \"failed\";\n\nexport type AvatarImageProps = Omit<\n  ImgHTMLAttributes<HTMLImageElement>,\n  | \"aria-hidden\"\n  | \"aria-label\"\n  | \"aria-labelledby\"\n  | \"alt\"\n  | \"children\"\n  | \"className\"\n  | \"crossOrigin\"\n  | \"decoding\"\n  | \"fetchPriority\"\n  | \"height\"\n  | \"loading\"\n  | \"onError\"\n  | \"onLoad\"\n  | \"referrerPolicy\"\n  | \"role\"\n  | \"sizes\"\n  | \"src\"\n  | \"srcSet\"\n  | \"style\"\n  | \"width\"\n>;\n\nexport interface AvatarProps extends Omit<\n  HTMLAttributes<HTMLSpanElement>,\n  \"aria-label\" | \"aria-labelledby\" | \"children\" | \"role\"\n> {\n  alt?: string;\n  crossOrigin?: ImgHTMLAttributes<HTMLImageElement>[\"crossOrigin\"];\n  decoding?: ImgHTMLAttributes<HTMLImageElement>[\"decoding\"];\n  decorative?: boolean;\n  fallbackIcon?: ReactNode;\n  fetchPriority?: ImgHTMLAttributes<HTMLImageElement>[\"fetchPriority\"];\n  imageProps?: AvatarImageProps;\n  initials?: string;\n  loading?: ImgHTMLAttributes<HTMLImageElement>[\"loading\"];\n  motion?: AvatarMotion;\n  name?: string;\n  onImageError?: ImgHTMLAttributes<HTMLImageElement>[\"onError\"];\n  onImageLoad?: ImgHTMLAttributes<HTMLImageElement>[\"onLoad\"];\n  referrerPolicy?: ImgHTMLAttributes<HTMLImageElement>[\"referrerPolicy\"];\n  reducedMotion?: boolean;\n  ring?: AvatarRing;\n  shape?: AvatarShape;\n  size?: AvatarSize;\n  sizes?: string;\n  src?: string;\n  srcSet?: string;\n  tone?: AvatarTone;\n}\n\nconst avatarBaseClasses =\n  \"relative inline-flex aspect-square shrink-0 select-none items-center justify-center overflow-hidden border align-middle font-semibold leading-none outline-none [rotate:0deg] [scale:1] [translate:0_0] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[reduced-motion=true]:will-change-auto\";\n\nconst avatarSizeClasses: Record<AvatarSize, string> = {\n  xs: \"[--avatar-icon-size:calc(var(--avatar-size)*0.48)] [--avatar-size:calc(var(--dt-density-control)*0.65)] size-[var(--avatar-size)] text-[0.625rem]\",\n  sm: \"[--avatar-icon-size:calc(var(--avatar-size)*0.48)] [--avatar-size:calc(var(--dt-density-control)*0.8)] size-[var(--avatar-size)] text-xs\",\n  md: \"[--avatar-icon-size:calc(var(--avatar-size)*0.48)] [--avatar-size:var(--dt-density-control)] size-[var(--avatar-size)] text-sm\",\n  lg: \"[--avatar-icon-size:calc(var(--avatar-size)*0.48)] [--avatar-size:calc(var(--dt-density-control)*1.2)] size-[var(--avatar-size)] text-base\",\n  xl: \"[--avatar-icon-size:calc(var(--avatar-size)*0.46)] [--avatar-size:calc(var(--dt-density-control)*1.45)] size-[var(--avatar-size)] text-lg\",\n  \"2xl\":\n    \"[--avatar-icon-size:calc(var(--avatar-size)*0.44)] [--avatar-size:calc(var(--dt-density-control)*1.75)] size-[var(--avatar-size)] text-xl\",\n};\n\nconst avatarShapeClasses: Record<AvatarShape, string> = {\n  circle: \"rounded-full\",\n  rounded: \"rounded-md\",\n  square: \"rounded-none\",\n};\n\nconst avatarToneClasses: Record<AvatarTone, string> = {\n  neutral: \"bg-muted text-foreground\",\n  primary: \"bg-primary/10 text-foreground\",\n  success: \"bg-success/10 text-foreground\",\n  warning: \"bg-warning/15 text-foreground\",\n  destructive: \"bg-destructive/10 text-foreground\",\n  info: \"bg-info/10 text-foreground\",\n};\n\nconst avatarRingClasses: Record<AvatarRing, string> = {\n  none: \"border-transparent\",\n  border: \"border-border contrast-more:border-current\",\n  ring: \"border-background ring-2 ring-ring/35 ring-offset-2 ring-offset-background contrast-more:border-current contrast-more:ring-current\",\n};\n\nconst avatarImageClasses = \"absolute inset-0 size-full object-cover\";\n\nconst avatarFallbackClasses =\n  \"absolute inset-0 flex size-full items-center justify-center text-center\";\n\nconst avatarInitialsClasses =\n  \"max-w-full truncate px-[var(--dt-space-0-5)] uppercase\";\n\nconst avatarIconClasses =\n  \"inline-flex size-[var(--avatar-icon-size)] items-center justify-center opacity-85 [&>svg]:size-full [&>svg]:shrink-0\";\n\nconst avatarMotionTargets: Record<\n  Exclude<AvatarMotion, \"none\">,\n  TargetAndTransition\n> = {\n  subtle: {\n    opacity: 0.96,\n    scale: 1.015,\n    transition: { type: \"spring\", stiffness: 420, damping: 36, mass: 0.8 },\n  },\n  standard: {\n    opacity: 0.94,\n    scale: 1.035,\n    transition: { type: \"spring\", stiffness: 480, damping: 34, mass: 0.75 },\n  },\n};\n\nconst reducedMotionTarget: TargetAndTransition = {\n  opacity: 0.96,\n  transition: { duration: 0 },\n};\n\nexport function avatarClassNames({\n  className,\n  ring = \"none\",\n  shape = \"circle\",\n  size = \"md\",\n  tone = \"neutral\",\n}: Pick<AvatarProps, \"className\" | \"ring\" | \"shape\" | \"size\" | \"tone\"> = {}) {\n  return cn(\n    avatarBaseClasses,\n    avatarSizeClasses[size],\n    avatarShapeClasses[shape],\n    avatarToneClasses[tone],\n    avatarRingClasses[ring],\n    \"contrast-more:bg-background contrast-more:text-foreground\",\n    className,\n  );\n}\n\nexport function getAvatarInitials(name: string, maxInitials = 2) {\n  if (!Number.isFinite(maxInitials)) {\n    maxInitials = 2;\n  }\n\n  const count = Math.min(Math.floor(maxInitials), 3);\n\n  if (count <= 0) {\n    return \"\";\n  }\n\n  const tokens = name\n    .trim()\n    .replace(/['’]/gu, \"\")\n    .replace(/[^\\p{L}\\p{N}]+/gu, \" \")\n    .split(/\\s+/u)\n    .filter(Boolean);\n\n  if (tokens.length === 0) {\n    return \"\";\n  }\n\n  const selected =\n    tokens.length === 1\n      ? Array.from(tokens[0]).slice(0, count)\n      : count === 1\n        ? [Array.from(tokens[0])[0]]\n        : count === 2 || tokens.length === 2\n          ? [Array.from(tokens[0])[0], Array.from(tokens[tokens.length - 1])[0]]\n          : [\n              Array.from(tokens[0])[0],\n              Array.from(tokens[1])[0],\n              Array.from(tokens[tokens.length - 1])[0],\n            ];\n\n  return selected.join(\"\").toUpperCase();\n}\n\nfunction normalizeInitials(initials: string) {\n  return Array.from(initials.trim().replace(/\\s+/gu, \"\"))\n    .slice(0, 3)\n    .join(\"\")\n    .toUpperCase();\n}\n\nfunction DefaultAvatarIcon() {\n  return (\n    <svg aria-hidden=\"true\" fill=\"none\" focusable=\"false\" viewBox=\"0 0 24 24\">\n      <path\n        d=\"M12 12.25a4.25 4.25 0 1 0 0-8.5 4.25 4.25 0 0 0 0 8.5Zm7.25 7.5a7.25 7.25 0 0 0-14.5 0\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.75\"\n      />\n    </svg>\n  );\n}\n\nfunction getMotionTarget({\n  motion,\n  reducedMotion,\n}: {\n  motion: AvatarMotion;\n  reducedMotion: boolean;\n}) {\n  if (motion === \"none\") {\n    return undefined;\n  }\n\n  if (reducedMotion) {\n    return reducedMotionTarget;\n  }\n\n  return avatarMotionTargets[motion];\n}\n\nfunction getMotionBehavior({\n  motion,\n  reducedMotion,\n}: {\n  motion: AvatarMotion;\n  reducedMotion: boolean;\n}) {\n  if (motion === \"none\") {\n    return \"disabled\";\n  }\n\n  return reducedMotion ? \"opacity\" : \"transform-opacity\";\n}\n\nexport const Avatar = forwardRef<HTMLSpanElement, AvatarProps>(\n  (\n    {\n      alt,\n      className,\n      crossOrigin,\n      decoding,\n      decorative = false,\n      fallbackIcon,\n      fetchPriority,\n      imageProps,\n      initials,\n      loading,\n      motion = \"standard\",\n      name,\n      onBlur,\n      onFocus,\n      onImageError,\n      onImageLoad,\n      onMouseEnter,\n      onMouseLeave,\n      onPointerEnter,\n      onPointerLeave,\n      referrerPolicy,\n      reducedMotion: reducedMotionProp,\n      ring = \"none\",\n      shape = \"circle\",\n      size = \"md\",\n      sizes,\n      src,\n      srcSet,\n      tone = \"neutral\",\n      ...props\n    },\n    ref,\n  ) => {\n    const prefersReducedMotion = useReducedMotion();\n    const hasHydrated = useHydrated();\n    const reducedMotion =\n      motion === \"none\" ||\n      reducedMotionProp === true ||\n      (hasHydrated && prefersReducedMotion === true);\n    const [imageResult, setImageResult] = useState({ src, failed: false });\n    const imageFailed = imageResult.src === src && imageResult.failed;\n    if (imageResult.src !== src) {\n      setImageResult({ src, failed: false });\n    }\n    const [motionActive, setMotionActive] = useState(false);\n    const hasImage = Boolean(src);\n    const showImage = hasImage && !imageFailed;\n    const imageState: AvatarImageState = showImage\n      ? \"image\"\n      : imageFailed\n        ? \"failed\"\n        : \"fallback\";\n    const isDecorative = decorative || alt === \"\";\n    const normalizedInitials =\n      initials !== undefined ? normalizeInitials(initials) : undefined;\n    const generatedInitials =\n      normalizedInitials === undefined && !fallbackIcon && name\n        ? getAvatarInitials(name)\n        : \"\";\n    const fallbackInitials = normalizedInitials ?? generatedInitials;\n    const accessibleLabel = isDecorative\n      ? undefined\n      : alt || name || fallbackInitials || undefined;\n    const imageAlt = isDecorative\n      ? \"\"\n      : (alt ?? name ?? fallbackInitials ?? \"\");\n    const rootRole = !showImage && accessibleLabel ? \"img\" : undefined;\n    const motionTarget = getMotionTarget({ motion, reducedMotion });\n    const motionBehavior = getMotionBehavior({ motion, reducedMotion });\n\n    const handleImageError: ImgHTMLAttributes<HTMLImageElement>[\"onError\"] = (\n      event,\n    ) => {\n      setImageResult({ src, failed: true });\n      onImageError?.(event);\n    };\n\n    const handleImageLoad: ImgHTMLAttributes<HTMLImageElement>[\"onLoad\"] = (\n      event,\n    ) => {\n      onImageLoad?.(event);\n    };\n\n    const handlePointerEnter: PointerEventHandler<HTMLSpanElement> = (\n      event,\n    ) => {\n      if (event.pointerType !== \"touch\") {\n        setMotionActive(true);\n      }\n\n      onPointerEnter?.(event);\n    };\n\n    const handlePointerLeave: PointerEventHandler<HTMLSpanElement> = (\n      event,\n    ) => {\n      setMotionActive(false);\n      onPointerLeave?.(event);\n    };\n\n    const handleMouseEnter: MouseEventHandler<HTMLSpanElement> = (event) => {\n      setMotionActive(true);\n      onMouseEnter?.(event);\n    };\n\n    const handleMouseLeave: MouseEventHandler<HTMLSpanElement> = (event) => {\n      setMotionActive(false);\n      onMouseLeave?.(event);\n    };\n\n    const handleFocus: FocusEventHandler<HTMLSpanElement> = (event) => {\n      setMotionActive(true);\n      onFocus?.(event);\n    };\n\n    const handleBlur: FocusEventHandler<HTMLSpanElement> = (event) => {\n      setMotionActive(false);\n      onBlur?.(event);\n    };\n\n    return (\n      <motionElement.span\n        {...(props as HTMLMotionProps<\"span\">)}\n        ref={ref}\n        aria-hidden={isDecorative ? true : undefined}\n        aria-label={rootRole ? accessibleLabel : undefined}\n        data-slot=\"avatar\"\n        data-size={size}\n        data-shape={shape}\n        data-tone={tone}\n        data-ring={ring}\n        data-motion={motion}\n        data-motion-behavior={motionBehavior}\n        data-motion-state={motionActive ? \"active\" : \"idle\"}\n        data-reduced-motion={reducedMotion ? \"true\" : undefined}\n        data-state={imageState}\n        role={rootRole}\n        className={avatarClassNames({ className, ring, shape, size, tone })}\n        initial={false}\n        whileFocus={motionTarget}\n        whileHover={motionTarget}\n        onBlur={handleBlur}\n        onFocus={handleFocus}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        onPointerEnter={handlePointerEnter}\n        onPointerLeave={handlePointerLeave}\n      >\n        <span\n          aria-hidden=\"true\"\n          data-slot=\"avatar-fallback\"\n          className={avatarFallbackClasses}\n        >\n          {fallbackInitials ? (\n            <span data-slot=\"avatar-initials\" className={avatarInitialsClasses}>\n              {fallbackInitials}\n            </span>\n          ) : (\n            <span data-slot=\"avatar-icon\" className={avatarIconClasses}>\n              {fallbackIcon ?? <DefaultAvatarIcon />}\n            </span>\n          )}\n        </span>\n        {showImage ? (\n          <img\n            {...imageProps}\n            alt={imageAlt}\n            className={avatarImageClasses}\n            crossOrigin={crossOrigin}\n            data-slot=\"avatar-image\"\n            decoding={decoding}\n            fetchPriority={fetchPriority}\n            loading={loading}\n            onError={handleImageError}\n            onLoad={handleImageLoad}\n            referrerPolicy={referrerPolicy}\n            sizes={sizes}\n            src={src}\n            srcSet={srcSet}\n          />\n        ) : null}\n      </motionElement.span>\n    );\n  },\n);\n\nAvatar.displayName = \"Avatar\";\n"
    },
    {
      "path": "packages/components/src/components/avatar/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/avatar/index.ts",
      "content": "export {\n  Avatar,\n  avatarClassNames,\n  getAvatarInitials,\n  type AvatarImageProps,\n  type AvatarImageState,\n  type AvatarMotion,\n  type AvatarProps,\n  type AvatarRing,\n  type AvatarShape,\n  type AvatarSize,\n  type AvatarTone,\n} from \"./avatar\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
