{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "achievement-badge",
  "title": "Achievement Badge",
  "description": "Open source React achievement badge component with locked/unlocked states, progress rings and rarity support. Use for achievement UI design and collections. Built on shadcn/ui + Tailwind.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/trophy/ui/achievement-badge.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Trophy } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface Achievement {\n  id: string\n  name: string\n  trigger: \"metric\" | \"api\" | \"streak\"\n  badgeUrl?: string | null\n  progress?: number\n  rarity?: number\n}\n\ninterface UserAchievement extends Achievement {\n  /** ISO date when earned, or `null` if locked */\n  achievedAt: string | null\n}\n\ninterface AchievementBadgeProps extends React.HTMLAttributes<HTMLDivElement> {\n  achievement: UserAchievement\n  badgeSize?: \"sm\" | \"default\" | \"lg\" | \"xl\"\n  onAchievementClick?: (achievement: UserAchievement) => void\n}\n\nconst badgeSizeMap = {\n  sm: \"h-12 w-12\",\n  default: \"h-16 w-16\",\n  lg: \"h-20 w-20\",\n  xl: \"h-28 w-28\",\n} as const\n\nconst iconSizeMap = {\n  sm: \"h-8 w-8\",\n  default: \"h-10 w-10\",\n  lg: \"h-12 w-12\",\n  xl: \"h-16 w-16\",\n} as const\n\nconst progressRingSizeMap = {\n  sm: 72,\n  default: 88,\n  lg: 104,\n  xl: 136,\n} as const\n\nconst AchievementBadge = React.forwardRef<\n  HTMLDivElement,\n  AchievementBadgeProps\n>(\n  (\n    {\n      className,\n      achievement,\n      badgeSize = \"default\",\n      onAchievementClick,\n      ...props\n    },\n    ref\n  ) => {\n    const isUnlocked = achievement.achievedAt !== null\n\n    const hasProgress = isUnlocked && typeof achievement.progress === \"number\"\n    const progress = hasProgress\n      ? Math.min(100, Math.max(0, achievement.progress ?? 0))\n      : 0\n    const hasRarity = typeof achievement.rarity === \"number\"\n    const rarity = hasRarity\n      ? Math.min(100, Math.max(1, Math.round(achievement.rarity ?? 1)))\n      : null\n    const ringSize = progressRingSizeMap[badgeSize]\n    const ringStrokeWidth = 4\n    const ringRadius = (ringSize - ringStrokeWidth) / 2\n    const ringCircumference = 2 * Math.PI * ringRadius\n    const ringDashoffset =\n      ringCircumference - (progress / 100) * ringCircumference\n\n    const statusLabel = isUnlocked ? \"Earned\" : \"Locked\"\n    const itemLabel = `${achievement.name} - ${statusLabel}`\n\n    return (\n      <div\n        ref={ref}\n        role={onAchievementClick ? \"button\" : \"listitem\"}\n        aria-label={onAchievementClick ? itemLabel : undefined}\n        tabIndex={onAchievementClick ? 0 : undefined}\n        onClick={() => onAchievementClick?.(achievement)}\n        onKeyDown={\n          onAchievementClick\n            ? (e) => {\n                if (e.key === \"Enter\" || e.key === \" \") {\n                  e.preventDefault()\n                  onAchievementClick(achievement)\n                }\n              }\n            : undefined\n        }\n        className={cn(\n          \"bg-card flex flex-col items-center justify-center gap-2 rounded-lg border p-4\",\n          onAchievementClick && \"cursor-pointer\",\n          !isUnlocked && \"opacity-50\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          className=\"relative flex items-center justify-center\"\n          style={{\n            width: hasProgress ? `${ringSize}px` : undefined,\n            height: hasProgress ? `${ringSize}px` : undefined,\n          }}\n        >\n          {hasProgress ? (\n            <svg\n              aria-hidden=\"true\"\n              className=\"absolute inset-0 h-full w-full\"\n              viewBox={`0 0 ${ringSize} ${ringSize}`}\n            >\n              <circle\n                cx={ringSize / 2}\n                cy={ringSize / 2}\n                r={ringRadius}\n                fill=\"none\"\n                stroke=\"var(--primary)\"\n                strokeLinecap=\"round\"\n                strokeWidth={ringStrokeWidth}\n                strokeDasharray={ringCircumference}\n                strokeDashoffset={ringDashoffset}\n                transform={`rotate(-90 ${ringSize / 2} ${ringSize / 2})`}\n              />\n            </svg>\n          ) : null}\n\n          {achievement.badgeUrl ? (\n            <img\n              src={achievement.badgeUrl}\n              alt={`${achievement.name} badge - ${statusLabel}`}\n              className={cn(\n                badgeSizeMap[badgeSize],\n                \"relative z-10 rounded-full object-cover\",\n                !isUnlocked && \"grayscale\"\n              )}\n            />\n          ) : (\n            <div\n              aria-hidden=\"true\"\n              className={cn(\n                badgeSizeMap[badgeSize],\n                \"relative z-10 flex items-center justify-center rounded-full\",\n                isUnlocked\n                  ? \"bg-muted text-muted-foreground\"\n                  : \"bg-primary text-primary-foreground\"\n              )}\n            >\n              <Trophy className={iconSizeMap[badgeSize]} />\n            </div>\n          )}\n        </div>\n\n        {rarity !== null ? (\n          <span className=\"text-muted-foreground text-xs font-medium\">\n            {rarity}% of users\n          </span>\n        ) : null}\n\n        <span\n          className={cn(\n            \"text-center text-sm leading-tight font-bold\",\n            !isUnlocked && \"text-muted-foreground\"\n          )}\n        >\n          {achievement.name}\n        </span>\n      </div>\n    )\n  }\n)\nAchievementBadge.displayName = \"AchievementBadge\"\n\nexport { AchievementBadge }\nexport type { AchievementBadgeProps, Achievement, UserAchievement }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}