{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "leaderboard-rankings",
  "title": "Leaderboard Rankings",
  "description": "React leaderboard rankings list—avatars, scores, bylines, and crown highlights for full tables. Open-source leaderboard UI for games, communities, and SaaS challenges. shadcn/ui + Tailwind.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/trophy/ui/leaderboard-rankings.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ChevronLeft,\n  ChevronRight,\n  CircleEllipsisIcon,\n  Crown,\n  EllipsisIcon,\n  TrendingDown,\n  TrendingUp,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\ninterface LeaderboardRankingItem {\n  userId: string\n  userName: string | null\n  rank: number\n  value: number\n  byline?: string | null\n  avatarUrl?: string | null\n  rankChange?: number\n  displayed?: boolean\n}\n\ninterface LeaderboardRankingsProps extends React.HTMLAttributes<HTMLDivElement> {\n  rankings: LeaderboardRankingItem[]\n  onUserClick?: (ranking: LeaderboardRankingItem) => void\n  currentUserId?: string\n  showPagination?: boolean\n  defaultPageSize?: 10 | 25 | 50 | 100\n}\n\nconst crownColorMap = {\n  1: \"text-rank-1\",\n  2: \"text-rank-2\",\n  3: \"text-rank-3\",\n} as const\n\nconst pageSizeOptions = [10, 25, 50, 100] as const\n\ntype LeaderboardRow =\n  | { type: \"ranking\"; ranking: LeaderboardRankingItem }\n  | { type: \"ellipsis\"; key: string }\n\nfunction formatLeaderboardValue(value: number) {\n  if (value >= 1000000) return `${(value / 1000000).toFixed(1)}m`\n  if (value >= 1000) return `${(value / 1000).toFixed(1)}k`\n  return value.toLocaleString()\n}\n\nconst LeaderboardRankings = React.forwardRef<\n  HTMLDivElement,\n  LeaderboardRankingsProps\n>(\n  (\n    {\n      className,\n      rankings,\n      onUserClick,\n      currentUserId,\n      showPagination = false,\n      defaultPageSize = 10,\n      ...props\n    },\n    ref\n  ) => {\n    const [pageSize, setPageSize] = React.useState<10 | 25 | 50 | 100>(\n      defaultPageSize\n    )\n    const [currentPage, setCurrentPage] = React.useState(1)\n\n    const totalPages = Math.max(1, Math.ceil(rankings.length / pageSize))\n\n    React.useEffect(() => {\n      setCurrentPage(1)\n    }, [pageSize])\n\n    React.useEffect(() => {\n      if (currentPage > totalPages) {\n        setCurrentPage(totalPages)\n      }\n    }, [currentPage, totalPages])\n\n    const pagedRankings = showPagination\n      ? rankings.slice((currentPage - 1) * pageSize, currentPage * pageSize)\n      : rankings\n\n    const rows = React.useMemo<LeaderboardRow[]>(() => {\n      const nextRows: LeaderboardRow[] = []\n      let hiddenRunCount = 0\n\n      pagedRankings.forEach((ranking, index) => {\n        const isDisplayed = ranking.displayed !== false\n        if (!isDisplayed) {\n          hiddenRunCount += 1\n          return\n        }\n\n        if (hiddenRunCount > 0) {\n          nextRows.push({ type: \"ellipsis\", key: `ellipsis-${index}` })\n          hiddenRunCount = 0\n        }\n\n        nextRows.push({ type: \"ranking\", ranking })\n      })\n\n      if (hiddenRunCount > 0) {\n        nextRows.push({ type: \"ellipsis\", key: \"ellipsis-tail\" })\n      }\n\n      return nextRows\n    }, [pagedRankings])\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"bg-card w-full rounded-xl border\", className)}\n        {...props}\n      >\n        <div\n          role=\"list\"\n          aria-label=\"Leaderboard rankings\"\n          className=\"divide-border divide-y\"\n        >\n          {rows.map((row) => {\n            if (row.type === \"ellipsis\") {\n              return (\n                <div\n                  key={row.key}\n                  role=\"listitem\"\n                  aria-label=\"Collapsed leaderboard rows\"\n                  className=\"text-muted-foreground flex items-center justify-center px-4 py-2\"\n                >\n                  <EllipsisIcon className=\"h-5 w-5\" />\n                </div>\n              )\n            }\n\n            const ranking = row.ranking\n            const displayName =\n              ranking.userName || `User ${ranking.userId.slice(0, 6)}`\n            const showCrown = ranking.rank <= 3\n            const crownColor = crownColorMap[ranking.rank as 1 | 2 | 3]\n            const isCurrentUser = currentUserId === ranking.userId\n\n            return (\n              <div\n                key={ranking.userId}\n                role=\"listitem\"\n                tabIndex={onUserClick ? 0 : undefined}\n                onClick={() => onUserClick?.(ranking)}\n                onKeyDown={\n                  onUserClick\n                    ? (e) => {\n                        if (e.key === \"Enter\" || e.key === \" \") {\n                          e.preventDefault()\n                          onUserClick(ranking)\n                        }\n                      }\n                    : undefined\n                }\n                className={cn(\n                  \"flex items-center gap-2 px-4 py-2\",\n                  isCurrentUser &&\n                    \"border-primary bg-muted rounded-md border-2\",\n                  onUserClick &&\n                    \"hover:bg-muted/40 cursor-pointer transition-colors\"\n                )}\n              >\n                <div className=\"flex w-12 items-center gap-1\">\n                  <span className=\"w-4 text-sm font-semibold tabular-nums\">\n                    {ranking.rank}\n                  </span>\n                  {showCrown ? (\n                    <Crown\n                      className={cn(\"h-5 w-5\", crownColor)}\n                      aria-hidden=\"true\"\n                    />\n                  ) : null}\n                </div>\n\n                {ranking.avatarUrl ? (\n                  <img\n                    src={ranking.avatarUrl}\n                    alt={`${displayName} avatar`}\n                    className=\"h-10 w-10 rounded-full object-cover\"\n                  />\n                ) : (\n                  <div className=\"bg-muted text-muted-foreground flex h-10 w-10 items-center justify-center rounded-full text-sm font-medium\">\n                    {(ranking.userName ?? ranking.userId)\n                      .charAt(0)\n                      .toUpperCase()}\n                  </div>\n                )}\n\n                <div className=\"min-w-0 flex-1\">\n                  <p className=\"text-foreground truncate font-medium\">\n                    {displayName}\n                  </p>\n                  {ranking.byline ? (\n                    <p className=\"text-muted-foreground truncate text-sm\">\n                      {ranking.byline}\n                    </p>\n                  ) : null}\n                </div>\n\n                <div className=\"flex items-center gap-2 text-right\">\n                  {typeof ranking.rankChange === \"number\" &&\n                  ranking.rankChange !== 0 ? (\n                    <p\n                      className={cn(\n                        \"inline-flex items-center gap-1 text-xs font-medium\",\n                        ranking.rankChange > 0\n                          ? \"text-success-600\"\n                          : \"text-red-600\"\n                      )}\n                    >\n                      {ranking.rankChange > 0 ? (\n                        <TrendingUp\n                          className=\"h-3.5 w-3.5\"\n                          aria-hidden=\"true\"\n                        />\n                      ) : (\n                        <TrendingDown\n                          className=\"h-3.5 w-3.5\"\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                      {Math.abs(ranking.rankChange)}\n                    </p>\n                  ) : null}\n                  <p className=\"leading-none font-semibold tabular-nums\">\n                    {formatLeaderboardValue(ranking.value)}\n                  </p>\n                </div>\n              </div>\n            )\n          })}\n        </div>\n\n        {showPagination ? (\n          <div className=\"flex items-center justify-between gap-3 border-t px-4 py-2\">\n            <div className=\"flex items-center gap-2\">\n              <label\n                htmlFor=\"leaderboard-page-size\"\n                className=\"text-muted-foreground text-sm\"\n              >\n                Show\n              </label>\n              <select\n                id=\"leaderboard-page-size\"\n                value={pageSize}\n                onChange={(e) =>\n                  setPageSize(Number(e.target.value) as 10 | 25 | 50 | 100)\n                }\n                className=\"bg-background text-muted-foreground rounded-md border px-2 py-1 text-sm\"\n              >\n                {pageSizeOptions.map((option) => (\n                  <option key={option} value={option}>\n                    {option}\n                  </option>\n                ))}\n              </select>\n            </div>\n\n            <div className=\"flex items-center gap-2\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}\n                disabled={currentPage === 1}\n                className=\"hover:bg-muted rounded-md border p-1.5 transition-colors disabled:cursor-not-allowed disabled:opacity-50\"\n              >\n                <ChevronLeft className=\"h-4 w-4\" />\n              </Button>\n\n              <span className=\"text-muted-foreground text-sm\">\n                Page {currentPage} of {totalPages}\n              </span>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                aria-label=\"Next page\"\n                onClick={() =>\n                  setCurrentPage((p) => Math.min(totalPages, p + 1))\n                }\n                disabled={currentPage === totalPages}\n                className=\"hover:bg-muted rounded-md border p-1.5 transition-colors disabled:cursor-not-allowed disabled:opacity-50\"\n              >\n                <ChevronRight className=\"h-4 w-4\" />\n              </Button>\n            </div>\n          </div>\n        ) : null}\n      </div>\n    )\n  }\n)\n\nLeaderboardRankings.displayName = \"LeaderboardRankings\"\n\nexport { LeaderboardRankings }\nexport type { LeaderboardRankingItem, LeaderboardRankingsProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}