{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-score-row",
  "title": "Score Row",
  "description": "A scored list item pairing icon + label with a fractional score and optional progress bar. Includes ScoreRowList container for grouped items.",
  "dependencies": [
    "lucide-react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@lema-ds/progress",
    "@lema-ds/skeleton",
    "@lema-ds/tooltip",
    "@lema-ds/ui-i18n",
    "@lema-ds/format-utils",
    "@lema-ds/ds-locale-provider",
    "@lema-ds/tokens"
  ],
  "files": [
    {
      "path": "components/ds/ds-score-row.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { FileTextIcon } from \"lucide-react\"\nimport { cva } from \"class-variance-authority\"\nimport type { HTMLAttributes } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { UI_I18N, type UILocale } from \"@/lib/ui-i18n\"\nimport { useUILocale } from \"@/components/ui/ds-locale-provider\"\nimport { formatValue } from \"@/lib/format-utils\"\nimport { Progress } from \"@/components/ui/progress\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport type ScoreRowSize = \"sm\" | \"md\" | \"lg\"\nexport type ScoreRowStatus = \"default\" | \"success\" | \"warning\" | \"destructive\"\nexport type ScoreRowScoreDisplay = \"fraction\" | \"percent\" | \"raw\"\n\nexport interface ScoreRowProps extends HTMLAttributes<HTMLElement> {\n  title: string\n  description?: string\n  /** Lucide icon component. Defaults to FileTextIcon. */\n  icon?: React.ElementType\n  /** Current score value. */\n  score: number\n  /** Maximum / total possible score. */\n  total: number\n  size?: ScoreRowSize\n  /**\n   * Color status for the icon container and score.\n   * Use `\"auto\"` to derive from the score ratio:\n   * ≥70% → success, ≥40% → warning, <40% → destructive.\n   */\n  status?: ScoreRowStatus | \"auto\"\n  /** How the score is rendered on the right. Defaults to \"fraction\". */\n  scoreDisplay?: ScoreRowScoreDisplay\n  /**\n   * When true, renders a colored progress bar at the bottom of the row.\n   * Hovering the bar shows a tooltip with locale-aware percentage.\n   */\n  showProgress?: boolean\n  loading?: boolean\n  locale?: UILocale\n  /** Number of decimal places in the percentage display (progress tooltip and aria-label). Defaults to 0. */\n  percentDecimals?: number\n  /**\n   * Override all intent-based colors via CSS token `--score-accent`.\n   * Drives icon container bg/text, score value text, and progress fill.\n   * Accepts any CSS color or `var(--my-token)`.\n   */\n  accent?: string\n  /**\n   * Optional tooltip content for the entire row. When provided, the row\n   * itself becomes the tooltip trigger (keyboard-focusable) and the\n   * built-in progress-bar tooltip is suppressed, avoiding nested tooltips.\n   */\n  tooltip?: React.ReactNode\n}\n\n// ── Variants ───────────────────────────────────────────────────────────────\n\nexport const scoreRowVariants = cva(\n  [\n    \"flex w-full flex-col border-b border-border\",\n    \"motion-safe:transition-colors motion-safe:duration-150\",\n  ],\n  {\n    variants: {\n      size: {\n        sm: \"gap-2 px-3 py-2\",\n        md: \"gap-2.5 px-4 py-3\",\n        lg: \"gap-3 px-5 py-4\",\n      },\n      interactive: {\n        true: \"cursor-pointer hover:bg-accent focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset\",\n        false: \"\",\n      },\n    },\n    defaultVariants: { size: \"md\", interactive: false },\n  }\n)\n\n// Horizontal gap between icon container and text stack\nconst ICON_TEXT_GAP: Record<ScoreRowSize, string> = {\n  sm: \"gap-2\",\n  md: \"gap-3\",\n  lg: \"gap-3\",\n}\n\n// Horizontal gap between content row and score — also between score parts\nconst CONTENT_GAP: Record<ScoreRowSize, string> = {\n  sm: \"gap-3\",\n  md: \"gap-4\",\n  lg: \"gap-5\",\n}\n\n// Gap between score parts (current / slash / total) for symmetric spacing\nconst SCORE_GAP: Record<ScoreRowSize, string> = {\n  sm: \"gap-0.5\",\n  md: \"gap-1\",\n  lg: \"gap-1\",\n}\n\nconst PROGRESS_HEIGHT: Record<ScoreRowSize, string> = {\n  sm: \"h-2\",\n  md: \"h-3\",\n  lg: \"h-4\",\n}\n\nexport const scoreRowIconContainerVariants = cva(\n  [\"flex shrink-0 items-center justify-center rounded-md\"],\n  {\n    variants: {\n      size: {\n        sm: \"size-7\",\n        md: \"size-8\",\n        lg: \"size-10\",\n      },\n      status: {\n        default: \"bg-muted text-muted-foreground\",\n        success: \"bg-success/10 text-success\",\n        warning: \"bg-warning/15 text-warning\",\n        destructive: \"bg-destructive/10 text-destructive\",\n      },\n    },\n    defaultVariants: { size: \"md\", status: \"default\" },\n  }\n)\n\nexport const scoreRowIconVariants = cva(\"shrink-0\", {\n  variants: {\n    size: {\n      // 50% fill ratio vs. the container (size-7/8/10)\n      sm: \"size-3.5\",\n      md: \"size-4\",\n      lg: \"size-5\",\n    },\n  },\n  defaultVariants: { size: \"md\" },\n})\n\nexport const scoreRowTitleVariants = cva(\n  [\"truncate leading-tight font-medium text-foreground\"],\n  {\n    variants: {\n      size: {\n        sm: \"text-xs\",\n        md: \"text-sm\",\n        lg: \"text-base\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nexport const scoreRowDescriptionVariants = cva(\n  \"truncate leading-tight text-muted-foreground\",\n  {\n    variants: {\n      size: {\n        // text-xs for sm (no arbitrary values) — color alone distinguishes from title\n        sm: \"text-xs\",\n        md: \"text-xs\",\n        lg: \"text-sm\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nexport const scoreRowCurrentVariants = cva(\"font-semibold tabular-nums\", {\n  variants: {\n    size: {\n      sm: \"text-xs\",\n      md: \"text-sm\",\n      lg: \"text-base\",\n    },\n    status: {\n      default: \"text-foreground\",\n      success: \"text-success\",\n      warning: \"text-warning\",\n      destructive: \"text-destructive\",\n    },\n  },\n  defaultVariants: { size: \"md\", status: \"default\" },\n})\n\nexport const scoreRowTotalVariants = cva(\"text-muted-foreground tabular-nums\", {\n  variants: {\n    size: {\n      sm: \"text-xs\",\n      md: \"text-sm\",\n      lg: \"text-base\",\n    },\n  },\n  defaultVariants: { size: \"md\" },\n})\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction deriveStatus(score: number, total: number): ScoreRowStatus {\n  if (total === 0) return \"default\"\n  const ratio = score / total\n  if (ratio >= 0.7) return \"success\"\n  if (ratio >= 0.4) return \"warning\"\n  return \"destructive\"\n}\n\nfunction formatScore(\n  score: number,\n  total: number,\n  display: ScoreRowScoreDisplay,\n  locale: UILocale,\n  decimals: number\n): { current: string; showFraction: boolean; rest: string } {\n  switch (display) {\n    case \"percent\":\n      return {\n        current:\n          total === 0\n            ? \"—\"\n            : formatValue(score / total, \"percent\", { decimals, locale }),\n        showFraction: false,\n        rest: \"\",\n      }\n    case \"raw\":\n      return {\n        current: formatValue(score, \"float\", { decimals, locale }),\n        showFraction: false,\n        rest: \"\",\n      }\n    case \"fraction\":\n    default:\n      return {\n        current: formatValue(score, \"float\", { decimals, locale }),\n        showFraction: true,\n        rest: formatValue(total, \"integer\", { locale }),\n      }\n  }\n}\n\nfunction formatScoreText(\n  score: number,\n  total: number,\n  display: ScoreRowScoreDisplay,\n  locale: UILocale,\n  decimals: number\n): string {\n  switch (display) {\n    case \"percent\":\n      return total === 0\n        ? \"—\"\n        : formatValue(score / total, \"percent\", { decimals, locale })\n    case \"raw\":\n      return formatValue(score, \"float\", { decimals, locale })\n    case \"fraction\":\n    default: {\n      const s = formatValue(score, \"float\", { decimals, locale })\n      const t = formatValue(total, \"integer\", { locale })\n      return `${s} / ${t}`\n    }\n  }\n}\n\n/** Locale-aware percent string, e.g. \"71,3%\" in pt-BR, \"71.3%\" in en-US. */\nfunction formatPercent(\n  ratio: number,\n  locale: UILocale,\n  decimals: number\n): string {\n  return formatValue(ratio, \"percent\", { decimals, locale })\n}\n\nconst PROGRESS_INDICATOR_COLOR: Record<ScoreRowStatus, string> = {\n  default: \"[&>[data-slot=progress-indicator]]:bg-primary\",\n  success: \"[&>[data-slot=progress-indicator]]:bg-success\",\n  warning: \"[&>[data-slot=progress-indicator]]:bg-warning\",\n  destructive: \"[&>[data-slot=progress-indicator]]:bg-destructive\",\n}\n\n// ── ScoreRow ───────────────────────────────────────────────────────────────\n\nexport function ScoreRow({\n  title,\n  description,\n  icon: Icon = FileTextIcon,\n  score,\n  total,\n  size = \"md\",\n  status = \"default\",\n  scoreDisplay = \"fraction\",\n  showProgress = true,\n  loading = false,\n  locale: localeProp,\n  percentDecimals = 0,\n  accent,\n  tooltip,\n  className,\n  onClick,\n  ...props\n}: ScoreRowProps) {\n  const locale = useUILocale(localeProp)\n  const tokenStyle =\n    accent !== undefined\n      ? ({ \"--score-accent\": accent } as React.CSSProperties)\n      : undefined\n  const t = UI_I18N[locale].scoreRow\n  const resolvedStatus: ScoreRowStatus =\n    status === \"auto\" ? deriveStatus(score, total) : status\n\n  const isInteractive = !!onClick\n  const ratio = total > 0 ? Math.min(score / total, 1) : 0\n  const { current, showFraction, rest } = formatScore(\n    score,\n    total,\n    scoreDisplay,\n    locale,\n    percentDecimals\n  )\n  const scoreText = formatScoreText(\n    score,\n    total,\n    scoreDisplay,\n    locale,\n    percentDecimals\n  )\n  const percentText = formatPercent(ratio, locale, percentDecimals)\n\n  if (loading) {\n    return (\n      <div\n        data-slot=\"score-row\"\n        className={cn(scoreRowVariants({ size }), className)}\n        role=\"status\"\n        aria-busy=\"true\"\n        aria-label={t.loading}\n      >\n        <div\n          className={cn(\n            \"flex w-full items-center justify-between\",\n            CONTENT_GAP[size]\n          )}\n        >\n          <div\n            className={cn(\n              \"flex min-w-0 flex-1 items-center\",\n              ICON_TEXT_GAP[size]\n            )}\n          >\n            <Skeleton\n              className={cn(\n                \"shrink-0 rounded-md\",\n                size === \"sm\" ? \"size-7\" : size === \"lg\" ? \"size-10\" : \"size-8\"\n              )}\n            />\n            <div className=\"flex flex-col gap-0.5\">\n              <Skeleton\n                className={cn(\n                  \"rounded\",\n                  size === \"sm\"\n                    ? \"h-3 w-28\"\n                    : size === \"lg\"\n                      ? \"h-5 w-40\"\n                      : \"h-4 w-32\"\n                )}\n              />\n              <Skeleton\n                className={cn(\n                  \"rounded\",\n                  size === \"sm\"\n                    ? \"h-3 w-20\"\n                    : size === \"lg\"\n                      ? \"h-4 w-28\"\n                      : \"h-3 w-24\"\n                )}\n              />\n            </div>\n          </div>\n          <Skeleton\n            className={cn(\n              \"shrink-0 rounded\",\n              size === \"sm\"\n                ? \"h-3 w-12\"\n                : size === \"lg\"\n                  ? \"h-5 w-16\"\n                  : \"h-4 w-14\"\n            )}\n          />\n        </div>\n        {showProgress && (\n          <Skeleton\n            className={cn(\"w-full rounded-full\", PROGRESS_HEIGHT[size])}\n          />\n        )}\n      </div>\n    )\n  }\n\n  const inner = (\n    <>\n      {/* Content row: icon + text + score */}\n      <div\n        className={cn(\n          \"flex w-full items-center justify-between\",\n          CONTENT_GAP[size]\n        )}\n      >\n        {/* Leading: icon container + text stack */}\n        <div\n          className={cn(\n            \"flex min-w-0 flex-1 items-center\",\n            ICON_TEXT_GAP[size]\n          )}\n        >\n          <div\n            className={cn(\n              scoreRowIconContainerVariants({ size, status: resolvedStatus }),\n              accent && \"bg-(--score-accent)/10 text-(--score-accent)\"\n            )}\n            aria-hidden=\"true\"\n          >\n            <Icon className={cn(scoreRowIconVariants({ size }))} />\n          </div>\n\n          <div className=\"flex min-w-0 flex-col gap-0.5\">\n            <span className={cn(scoreRowTitleVariants({ size }))} title={title}>\n              {title}\n            </span>\n            {description && (\n              <span\n                className={cn(scoreRowDescriptionVariants({ size }))}\n                title={description}\n              >\n                {description}\n              </span>\n            )}\n          </div>\n        </div>\n\n        {/*\n         * Trailing: score — three visually styled spans with symmetric gap.\n         * All visual spans are aria-hidden; a sr-only span carries the accessible label\n         * so screen readers get a clean, locale-aware phrase instead of \"0 slash 95\".\n         */}\n        <div className={cn(\"flex shrink-0 items-baseline\", SCORE_GAP[size])}>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              scoreRowCurrentVariants({ size, status: resolvedStatus }),\n              accent && \"text-(--score-accent)\"\n            )}\n          >\n            {current}\n          </span>\n          {showFraction && (\n            <>\n              <span\n                aria-hidden=\"true\"\n                className={cn(scoreRowTotalVariants({ size }))}\n              >\n                /\n              </span>\n              <span\n                aria-hidden=\"true\"\n                className={cn(scoreRowTotalVariants({ size }))}\n              >\n                {rest}\n              </span>\n            </>\n          )}\n          <span className=\"sr-only\">\n            {t.scoreLabel}: {scoreText}\n          </span>\n        </div>\n      </div>\n\n      {/*\n       * Progress bar. When no row-level `tooltip` is set, it's wrapped in its\n       * own Tooltip showing locale-aware percentage with correct decimal\n       * separator (e.g. \"68 / 95 — 71,3%\" in pt-BR, \"68 / 95 — 71.3%\" in\n       * en-US). When `tooltip` is set, this per-bar tooltip is suppressed —\n       * the row-level tooltip below already covers it, avoiding nesting.\n       */}\n      {showProgress &&\n        (() => {\n          const progress = (\n            <Progress\n              value={ratio * 100}\n              aria-label={`${t.scoreLabel}: ${percentText}`}\n              className={cn(\n                PROGRESS_HEIGHT[size],\n                !accent && PROGRESS_INDICATOR_COLOR[resolvedStatus],\n                accent &&\n                  \"[&>[data-slot=progress-indicator]]:bg-(--score-accent)\"\n              )}\n            />\n          )\n\n          if (tooltip) {\n            return <span className=\"block\">{progress}</span>\n          }\n\n          return (\n            <TooltipProvider>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <span\n                    tabIndex={0}\n                    className=\"block rounded-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n                  >\n                    {progress}\n                  </span>\n                </TooltipTrigger>\n                <TooltipContent>\n                  <span className=\"tabular-nums\">\n                    {score} / {total} — {percentText}\n                  </span>\n                </TooltipContent>\n              </Tooltip>\n            </TooltipProvider>\n          )\n        })()}\n    </>\n  )\n\n  let row: React.ReactElement\n\n  if (isInteractive) {\n    const buttonLabel = [title, description, `${t.scoreLabel}: ${scoreText}`]\n      .filter(Boolean)\n      .join(\". \")\n\n    row = (\n      <button\n        type=\"button\"\n        data-slot=\"score-row\"\n        style={tokenStyle}\n        onClick={onClick}\n        className={cn(\n          scoreRowVariants({ size, interactive: true }),\n          \"w-full text-left\",\n          className\n        )}\n        aria-label={buttonLabel}\n      >\n        {inner}\n      </button>\n    )\n  } else {\n    row = (\n      <div\n        data-slot=\"score-row\"\n        style={tokenStyle}\n        tabIndex={tooltip ? 0 : undefined}\n        className={cn(\n          scoreRowVariants({ size }),\n          tooltip &&\n            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-inset\",\n          className\n        )}\n        {...props}\n      >\n        {inner}\n      </div>\n    )\n  }\n\n  if (!tooltip) return row\n\n  return (\n    <TooltipProvider delayDuration={100}>\n      <Tooltip>\n        <TooltipTrigger asChild>{row}</TooltipTrigger>\n        <TooltipContent>{tooltip}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  )\n}\n\nScoreRow.displayName = \"ScoreRow\"\n\n// ── ScoreRowList ───────────────────────────────────────────────────────────\n\nexport interface ScoreRowListProps extends HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode\n  /** Removes outer border to embed inside already-bordered containers. */\n  inset?: boolean\n}\n\nexport function ScoreRowList({\n  children,\n  inset = false,\n  className,\n  ...props\n}: ScoreRowListProps) {\n  return (\n    <div\n      data-slot=\"score-row-list\"\n      className={cn(\n        \"w-full\",\n        !inset && \"overflow-hidden rounded-xl border border-border\",\n        \"[&>*:last-child]:border-b-0\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nScoreRowList.displayName = \"ScoreRowList\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
