{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-progress-circular",
  "title": "Progress Circular",
  "description": "Displays progress as a circular gauge with an animated stroke and centered value.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@lema-ds/ui-i18n",
    "@lema-ds/format-utils",
    "@lema-ds/skeleton",
    "@lema-ds/ds-locale-provider"
  ],
  "files": [
    {
      "path": "components/ds/ds-progress-circular.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport type { HTMLAttributes } from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { UI_I18N, type UILocale } from \"@/lib/ui-i18n\"\nimport { useUILocale } from \"@/components/ui/ds-locale-provider\"\nimport { formatValue } from \"@/lib/format-utils\"\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport type ProgressCircularIntent =\n  \"primary\" | \"secondary\" | \"success\" | \"destructive\"\n\nexport type ProgressCircularSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport interface ProgressCircularVariantProps\n  extends\n    VariantProps<typeof progressCircularWrapperVariants>,\n    VariantProps<typeof progressCircularValueVariants>,\n    VariantProps<typeof progressCircularTitleVariants> {}\n\nexport interface ProgressCircularProps\n  extends ProgressCircularVariantProps, HTMLAttributes<HTMLDivElement> {\n  value: number\n  title?: string\n  intent?: ProgressCircularIntent\n  size?: ProgressCircularSize\n  loading?: boolean\n  precision?: number\n  locale?: UILocale\n  /**\n   * Override indicator stroke via CSS token `--progress-fill`.\n   * Accepts any CSS color value or `var(--my-token)`.\n   */\n  fillColor?: string\n  /**\n   * Override track stroke via CSS token `--progress-track`.\n   * Accepts any CSS color value or `var(--my-token)`.\n   */\n  trackColor?: string\n}\n\n// ── Variants ───────────────────────────────────────────────────────────────\n\nexport const progressCircularContainerVariants = cva(\n  \"flex flex-col items-center justify-center\"\n)\n\nexport const progressCircularWrapperVariants = cva(\n  \"relative flex items-center justify-center\",\n  {\n    variants: {\n      size: {\n        sm: \"size-20\",\n        md: \"size-30\",\n        lg: \"size-40\",\n        xl: \"size-50\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nexport const progressCircularSvgVariants = cva(\"-rotate-90\")\n\nexport const progressCircularIndicatorVariants = cva(\n  \"transition-[stroke-dashoffset] duration-500 ease-out\"\n)\n\nexport const progressCircularCenterTextVariants = cva(\n  \"absolute flex flex-col items-center justify-center rounded-full\",\n  {\n    variants: {\n      size: {\n        sm: \"size-10\",\n        md: \"size-15\",\n        lg: \"size-20\",\n        xl: \"size-25\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nexport const progressCircularValueVariants = cva(\n  \"font-semibold text-foreground tabular-nums\",\n  {\n    variants: {\n      size: {\n        sm: \"text-xs\",\n        md: \"text-sm\",\n        lg: \"text-lg\",\n        xl: \"text-2xl\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nconst progressCircularValueSkeletonDims: Record<ProgressCircularSize, string> =\n  {\n    sm: \"h-3 w-10\",\n    md: \"h-4 w-14\",\n    lg: \"h-5 w-16\",\n    xl: \"h-7 w-20\",\n  }\n\nconst progressCircularTitleSkeletonDims: Record<ProgressCircularSize, string> =\n  {\n    sm: \"h-3 w-16\",\n    md: \"h-4 w-24\",\n    lg: \"h-5 w-32\",\n    xl: \"h-6 w-40\",\n  }\n\nexport const progressCircularTitleVariants = cva(\n  \"mt-3 min-h-5 w-full text-center font-medium wrap-break-word text-muted-foreground\",\n  {\n    variants: {\n      size: {\n        sm: \"max-w-[80px] text-xs\",\n        md: \"max-w-[120px] text-sm\",\n        lg: \"max-w-[160px] text-base\",\n        xl: \"max-w-[200px] text-lg\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  }\n)\n\nexport const progressCircularStrokeVariants = cva(\"\", {\n  variants: {\n    intent: {\n      primary: \"stroke-primary\",\n      secondary: \"stroke-secondary\",\n      success: \"stroke-[var(--color-success)]\",\n      destructive: \"stroke-destructive\",\n    },\n  },\n  defaultVariants: { intent: \"primary\" },\n})\n\n// ── Constants ──────────────────────────────────────────────────────────────\n\nconst STROKE_WIDTH = 10\nconst RADIUS = 50\nconst VIEW_BOX = 120\nconst CIRCUMFERENCE = 2 * Math.PI * RADIUS\n\n// ── ProgressCircular ───────────────────────────────────────────────────────\n\nexport const ProgressCircular = React.forwardRef<\n  HTMLDivElement,\n  ProgressCircularProps\n>(\n  (\n    {\n      value,\n      title,\n      intent = \"primary\",\n      size = \"md\",\n      loading = false,\n      locale: localeProp,\n      precision = 0,\n      fillColor,\n      trackColor,\n      style,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const locale = useUILocale(localeProp)\n    const tokenStyle = {\n      ...(fillColor !== undefined && { \"--progress-fill\": fillColor }),\n      ...(trackColor !== undefined && { \"--progress-track\": trackColor }),\n      ...style,\n    } as React.CSSProperties\n    const [offset, setOffset] = React.useState(CIRCUMFERENCE)\n    const rafRef = React.useRef<number>(0)\n\n    const clamped = Math.max(0, Math.min(1, value > 1 ? value / 100 : value))\n\n    React.useEffect(() => {\n      const timer = setTimeout(() => {\n        const start = performance.now()\n        const duration = 500\n\n        const animate = (time: number) => {\n          const progress = Math.min((time - start) / duration, 1)\n          setOffset(CIRCUMFERENCE - clamped * CIRCUMFERENCE * progress)\n          if (progress < 1) rafRef.current = requestAnimationFrame(animate)\n        }\n\n        rafRef.current = requestAnimationFrame(animate)\n      }, 500)\n\n      return () => {\n        clearTimeout(timer)\n        if (rafRef.current) cancelAnimationFrame(rafRef.current)\n      }\n    }, [clamped])\n\n    const percentage = formatValue(clamped, \"percent\", {\n      decimals: precision,\n      locale,\n    })\n\n    return (\n      <div\n        ref={ref}\n        style={tokenStyle}\n        className={cn(progressCircularContainerVariants(), className)}\n        data-slot=\"progress-circular\"\n        {...props}\n      >\n        <div\n          className={cn(progressCircularWrapperVariants({ size }))}\n          role=\"progressbar\"\n          aria-valuenow={Math.round(clamped * 100)}\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-label={title || UI_I18N[locale].progressCircular.label}\n          data-slot=\"progress-circular-svg\"\n        >\n          <svg\n            width=\"100%\"\n            height=\"100%\"\n            viewBox={`0 0 ${VIEW_BOX} ${VIEW_BOX}`}\n            className={cn(progressCircularSvgVariants())}\n          >\n            <circle\n              cx=\"60\"\n              cy=\"60\"\n              r={RADIUS}\n              fill=\"none\"\n              stroke=\"currentColor\"\n              className={cn(\n                \"text-muted/30\",\n                trackColor && \"text-(--progress-track)\"\n              )}\n              strokeWidth={STROKE_WIDTH}\n              data-slot=\"progress-circular-track\"\n            />\n            <circle\n              cx=\"60\"\n              cy=\"60\"\n              r={RADIUS}\n              fill=\"none\"\n              strokeWidth={STROKE_WIDTH}\n              strokeDasharray={CIRCUMFERENCE}\n              strokeDashoffset={offset}\n              strokeLinecap=\"round\"\n              className={cn(\n                progressCircularIndicatorVariants(),\n                !fillColor && progressCircularStrokeVariants({ intent }),\n                fillColor && \"stroke-(--progress-fill)\"\n              )}\n              data-slot=\"progress-circular-indicator\"\n            />\n          </svg>\n\n          <div\n            className={cn(progressCircularCenterTextVariants({ size }))}\n            data-slot=\"progress-circular-center-text\"\n          >\n            {loading ? (\n              <Skeleton\n                className={cn(\n                  \"rounded\",\n                  progressCircularValueSkeletonDims[size]\n                )}\n              />\n            ) : (\n              <span className={cn(progressCircularValueVariants({ size }))}>\n                {percentage}\n              </span>\n            )}\n          </div>\n        </div>\n\n        {title &&\n          (loading ? (\n            <Skeleton\n              className={cn(\n                \"mx-auto rounded\",\n                progressCircularTitleSkeletonDims[size]\n              )}\n              data-slot=\"progress-circular-title\"\n            />\n          ) : (\n            <span\n              className={cn(progressCircularTitleVariants({ size }))}\n              data-slot=\"progress-circular-title\"\n            >\n              {title}\n            </span>\n          ))}\n      </div>\n    )\n  }\n)\n\nProgressCircular.displayName = \"ProgressCircular\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
