{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "points-levels-timeline",
  "title": "Points Levels Timeline",
  "description": "Vertical React timeline for level progression. Supports sub-levels, thresholds, and level progress for gamification roadmaps. Complements Points Levels List. Built on shadcn/ui + Tailwind. Open source.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/trophy/ui/points-levels-timeline.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Star } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface PointsSubLevelTimeline {\n  name: string\n  points: number\n}\n\nexport interface PointsLevelTimeline {\n  id: string\n  key?: string\n  name: string\n  description?: string\n  badgeUrl?: string | null\n  points: number\n  subLevels?: PointsSubLevelTimeline[]\n}\n\ninterface PointsLevelsTimelineProps extends React.HTMLAttributes<HTMLDivElement> {\n  levels: PointsLevelTimeline[]\n  currentPoints?: number\n  currentLevelLabel?: string\n  formatPoints?: (value: number) => string\n}\n\nfunction formatRangeLabel(\n  points: number,\n  nextLevelPoints: number | null | undefined,\n  formatPoints?: (value: number) => string\n) {\n  const format = formatPoints ?? ((value: number) => value.toLocaleString())\n  if (typeof nextLevelPoints === \"number\") {\n    return `${format(points)} - ${format(nextLevelPoints - 1)} XP`\n  }\n  return `${format(points)}+ XP`\n}\n\nconst PointsLevelsTimeline = React.forwardRef<\n  HTMLDivElement,\n  PointsLevelsTimelineProps\n>(\n  (\n    {\n      className,\n      levels,\n      currentPoints,\n      currentLevelLabel = \"Current\",\n      formatPoints,\n      ...props\n    },\n    ref\n  ) => {\n    const hasTracking = typeof currentPoints === \"number\"\n    const safeTotalPoints = hasTracking ? Math.max(0, currentPoints) : null\n\n    const currentLevelIndex = React.useMemo(() => {\n      if (!hasTracking || safeTotalPoints === null || levels.length === 0)\n        return -1\n      for (let i = levels.length - 1; i >= 0; i -= 1) {\n        if (safeTotalPoints >= levels[i].points) return i\n      }\n      return -1\n    }, [hasTracking, levels, safeTotalPoints])\n\n    const clampedCurrentProgress = React.useMemo(() => {\n      if (currentLevelIndex < 0 || safeTotalPoints === null) return 0\n      if (levels.length <= 1 || currentLevelIndex >= levels.length - 1)\n        return 100\n\n      const currentPointsThreshold = levels[currentLevelIndex].points\n      const nextPointsThreshold = levels[currentLevelIndex + 1].points\n      const segmentSize = Math.max(\n        1,\n        nextPointsThreshold - currentPointsThreshold\n      )\n      const progressInSegment = safeTotalPoints - currentPointsThreshold\n      return Math.max(0, Math.min((progressInSegment / segmentSize) * 100, 100))\n    }, [currentLevelIndex, levels, safeTotalPoints])\n\n    const progressHeightPercent = React.useMemo(() => {\n      if (currentLevelIndex < 0) return 0\n      if (levels.length <= 1) return 100\n      const segmentCount = levels.length - 1\n      if (currentLevelIndex >= levels.length - 1) return 100\n      const completedSegments = Math.max(0, currentLevelIndex)\n      const currentSegmentFraction = clampedCurrentProgress / 100\n      return Math.max(\n        0,\n        Math.min(\n          ((completedSegments + currentSegmentFraction) / segmentCount) * 100,\n          100\n        )\n      )\n    }, [clampedCurrentProgress, currentLevelIndex, levels.length])\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"bg-card w-full rounded-xl border p-4\", className)}\n        {...props}\n      >\n        <div className=\"relative\">\n          <div\n            aria-hidden=\"true\"\n            className=\"bg-muted absolute top-8 bottom-8 left-[17px] w-1 rounded-full\"\n          />\n          {hasTracking && currentLevelIndex >= 0 ? (\n            <div\n              aria-hidden=\"true\"\n              className=\"bg-primary absolute top-8 left-[17px] w-1 rounded-full transition-all duration-300\"\n              style={{\n                height: `calc((100% - 4rem) * ${progressHeightPercent / 100})`,\n              }}\n            />\n          ) : null}\n\n          <div role=\"list\" aria-label=\"Points levels\" className=\"space-y-6\">\n            {levels.map((level, index) => {\n              const isCurrent = currentLevelIndex === index\n              const isUnlocked =\n                currentLevelIndex >= 0 && index <= currentLevelIndex\n\n              return (\n                <div\n                  key={level.id}\n                  role=\"listitem\"\n                  className=\"grid grid-cols-[2.5rem_minmax(0,1fr)_auto] gap-4\"\n                >\n                  <div className=\"relative flex justify-center\">\n                    <span\n                      aria-hidden=\"true\"\n                      className={cn(\n                        \"z-10 inline-flex h-9 w-9 items-center justify-center rounded-full border-2\",\n                        isUnlocked\n                          ? \"border-primary bg-primary text-primary-foreground\"\n                          : \"border-border bg-muted text-muted-foreground\"\n                      )}\n                    >\n                      <Star className=\"h-4 w-4\" />\n                    </span>\n                  </div>\n\n                  <div className=\"min-w-0 space-y-2\">\n                    <div className=\"flex flex-wrap items-center gap-2\">\n                      <h3 className=\"text-foreground text-lg font-semibold\">\n                        {level.name}\n                      </h3>\n                      {isCurrent ? (\n                        <span className=\"bg-primary/10 text-primary rounded-full px-3 py-1 text-sm font-medium\">\n                          {currentLevelLabel}\n                        </span>\n                      ) : null}\n                    </div>\n                    {level.description ? (\n                      <p className=\"text-muted-foreground text-sm\">\n                        {level.description}\n                      </p>\n                    ) : null}\n                    {level.subLevels && level.subLevels.length > 0 ? (\n                      <div className=\"space-y-1 pt-1\">\n                        {level.subLevels.map((subLevel, subIndex) => {\n                          const nextSubLevelPoints =\n                            subIndex < level.subLevels!.length - 1\n                              ? level.subLevels![subIndex + 1].points\n                              : index < levels.length - 1\n                                ? levels[index + 1].points\n                                : null\n                          return (\n                            <p\n                              key={`${level.id}-${subLevel.name}`}\n                              className=\"text-muted-foreground text-sm\"\n                            >\n                              <span className=\"text-foreground font-medium\">\n                                {subLevel.name}\n                              </span>{\" \"}\n                              {formatRangeLabel(\n                                subLevel.points,\n                                nextSubLevelPoints,\n                                formatPoints\n                              )}\n                            </p>\n                          )\n                        })}\n                      </div>\n                    ) : null}\n                  </div>\n\n                  <p className=\"text-muted-foreground pt-1 text-lg font-semibold whitespace-nowrap\">\n                    {formatRangeLabel(\n                      level.points,\n                      index < levels.length - 1\n                        ? levels[index + 1].points\n                        : null,\n                      formatPoints\n                    )}\n                  </p>\n                </div>\n              )\n            })}\n          </div>\n        </div>\n      </div>\n    )\n  }\n)\n\nPointsLevelsTimeline.displayName = \"PointsLevelsTimeline\"\n\nexport { PointsLevelsTimeline }\nexport type { PointsLevelsTimelineProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}