{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "circular-progress",
  "title": "Circular Progress",
  "description": "The circular progress indicator, determinate and indeterminate.",
  "dependencies": [
    "@base-ui/react@^1.7.0"
  ],
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/circular-progress.tsx",
      "content": "\"use client\"\n\nimport { Progress as ProgressPrimitive } from \"@base-ui/react/progress\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype CircularProgressVariant = \"flat\" | \"wavy\"\ntype CircularProgressThickness = 4 | 8\n\ntype CircularProgressProps = Omit<\n  ProgressPrimitive.Root.Props,\n  \"children\" | \"value\"\n> & {\n  disabled?: boolean\n  value?: number | null\n  variant?: CircularProgressVariant\n  thickness?: CircularProgressThickness\n}\n\nconst TRACK_GAP = 4\n\n// Extracted from the kit's full circular wave vectors: six radial lobes on a\n// 20dp centreline, moving 2dp inward and outward. Sixteen line segments per\n// lobe keep the SVG silhouette smooth without baking a large Figma path into\n// the component.\nconst WAVE_LOBE_COUNT = 6\nconst WAVE_RADIUS = 20\nconst WAVE_AMPLITUDE = 2\nconst WAVE_SEGMENTS_PER_LOBE = 16\n\ntype PathPosition = {\n  anglePercent: number\n  radius: number\n}\n\nfunction createCirclePath(size: number, radius: number) {\n  const center = size / 2\n\n  return [\n    `M${center + radius} ${center}`,\n    `A${radius} ${radius} 0 1 1 ${center - radius} ${center}`,\n    `A${radius} ${radius} 0 1 1 ${center + radius} ${center}`,\n    \"Z\",\n  ].join(\" \")\n}\n\nfunction createWavyCircleGeometry(size: number) {\n  const center = size / 2\n  const segmentCount = WAVE_LOBE_COUNT * WAVE_SEGMENTS_PER_LOBE\n  const points = Array.from({ length: segmentCount + 1 }, (_, index) => {\n    const progress = index / segmentCount\n    const angle = progress * Math.PI * 2\n    const radius =\n      WAVE_RADIUS +\n      Math.cos(progress * Math.PI * 2 * WAVE_LOBE_COUNT) * WAVE_AMPLITUDE\n\n    return {\n      x: center + Math.cos(angle) * radius,\n      y: center + Math.sin(angle) * radius,\n    }\n  })\n  const segmentLengths = points.slice(1).map((point, index) =>\n    Math.hypot(point.x - points[index].x, point.y - points[index].y),\n  )\n  const totalLength = segmentLengths.reduce((sum, length) => sum + length, 0)\n  const path = `${points\n    .map(\n      ({ x, y }, index) =>\n        `${index === 0 ? \"M\" : \"L\"}${x.toFixed(3)} ${y.toFixed(3)}`,\n    )\n    .join(\" \")} Z`\n\n  function positionAtPercent(percent: number): PathPosition {\n    if (percent >= 100) {\n      return { anglePercent: 100, radius: WAVE_RADIUS + WAVE_AMPLITUDE }\n    }\n\n    const targetLength = (Math.max(0, percent) / 100) * totalLength\n    let traversedLength = 0\n\n    for (const [index, segmentLength] of segmentLengths.entries()) {\n      if (traversedLength + segmentLength < targetLength) {\n        traversedLength += segmentLength\n        continue\n      }\n\n      const segmentProgress =\n        segmentLength === 0\n          ? 0\n          : (targetLength - traversedLength) / segmentLength\n      const start = points[index]\n      const end = points[index + 1]\n      const x = start.x + (end.x - start.x) * segmentProgress\n      const y = start.y + (end.y - start.y) * segmentProgress\n\n      return {\n        anglePercent: ((index + segmentProgress) / segmentCount) * 100,\n        radius: Math.hypot(x - center, y - center),\n      }\n    }\n\n    return { anglePercent: 100, radius: WAVE_RADIUS + WAVE_AMPLITUDE }\n  }\n\n  return { path, positionAtPercent }\n}\n\nconst flatPositionAtPercent = (radius: number, percent: number): PathPosition => ({\n  anglePercent: percent,\n  radius,\n})\nconst wavy4Geometry = createWavyCircleGeometry(48)\nconst wavy8Geometry = createWavyCircleGeometry(52)\n\nconst geometry = {\n  flat: {\n    4: {\n      className: \"size-10\",\n      indicatorPath: createCirclePath(40, 18),\n      positionAtPercent: (percent: number) => flatPositionAtPercent(18, percent),\n      size: 40,\n      trackRadius: 18,\n    },\n    8: {\n      className: \"size-11\",\n      indicatorPath: createCirclePath(44, 18),\n      positionAtPercent: (percent: number) => flatPositionAtPercent(18, percent),\n      size: 44,\n      trackRadius: 18,\n    },\n  },\n  wavy: {\n    4: {\n      className: \"size-12\",\n      indicatorPath: wavy4Geometry.path,\n      positionAtPercent: wavy4Geometry.positionAtPercent,\n      size: 48,\n      trackRadius: 22,\n    },\n    8: {\n      className: \"size-13\",\n      indicatorPath: wavy8Geometry.path,\n      positionAtPercent: wavy8Geometry.positionAtPercent,\n      size: 52,\n      trackRadius: 22,\n    },\n  },\n} as const\n\nfunction getGapPercent(\n  indicatorRadius: number,\n  trackRadius: number,\n  thickness: CircularProgressThickness,\n) {\n  const centerDistance = TRACK_GAP + thickness\n  const cosine =\n    (indicatorRadius ** 2 + trackRadius ** 2 - centerDistance ** 2) /\n    (2 * indicatorRadius * trackRadius)\n\n  return (Math.acos(Math.min(1, Math.max(-1, cosine))) / (Math.PI * 2)) * 100\n}\n\n/**\n * Material 3 circular progress.\n *\n * Omit `value` (or pass `null`) for indeterminate progress. Determinate values\n * are normalized between `min` and `max`, with Base UI supplying the native\n * progressbar semantics. Give every indicator an accessible name with\n * `aria-label` or `aria-labelledby`.\n *\n * The flat and Expressive wavy variants use the kit's 40/44dp and 48/52dp\n * geometries for 4dp and 8dp strokes, respectively.\n */\nfunction CircularProgress({\n  className,\n  value = null,\n  min = 0,\n  max = 100,\n  variant = \"flat\",\n  thickness = 4,\n  disabled = false,\n  ...props\n}: CircularProgressProps) {\n  const selectedGeometry = geometry[variant][thickness]\n  const percent =\n    typeof value === \"number\" && max > min\n      ? Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100))\n      : null\n  // A round cap extends half a stroke beyond each path endpoint. The angular\n  // offsets preserve a 4dp straight-line gap between the two cap edges, even\n  // when a wavy endpoint sits inside or outside the track radius.\n  const indicatorEnd =\n    percent === null ? null : selectedGeometry.positionAtPercent(percent)\n  const trackStartGap =\n    indicatorEnd === null\n      ? 0\n      : getGapPercent(\n          indicatorEnd.radius,\n          selectedGeometry.trackRadius,\n          thickness,\n        )\n  const indicatorStart = selectedGeometry.positionAtPercent(0)\n  const trackEndGap = getGapPercent(\n    indicatorStart.radius,\n    selectedGeometry.trackRadius,\n    thickness,\n  )\n  const visibleTrack =\n    percent === null || percent <= 0\n      ? 100\n      : Math.max(\n          0,\n          100 -\n            indicatorEnd!.anglePercent -\n            trackStartGap -\n            trackEndGap,\n        )\n  const trackOffset =\n    percent === null || percent <= 0\n      ? 0\n      : -(indicatorEnd!.anglePercent + trackStartGap)\n\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"circular-progress\"\n      value={value}\n      min={min}\n      max={max}\n      data-variant={variant}\n      data-thickness={thickness}\n      data-disabled={disabled ? \"\" : undefined}\n      aria-disabled={disabled || undefined}\n      className={cn(\n        \"inline-flex shrink-0\",\n        selectedGeometry.className,\n        className,\n      )}\n      {...props}\n    >\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-full -rotate-90 overflow-visible\"\n        viewBox={`0 0 ${selectedGeometry.size} ${selectedGeometry.size}`}\n      >\n        <circle\n          data-slot=\"circular-progress-track\"\n          className={cn(\n            \"m3-circular-progress-track-transition stroke-m3-secondary-container\",\n            disabled && \"stroke-m3-on-surface/12\",\n          )}\n          cx={selectedGeometry.size / 2}\n          cy={selectedGeometry.size / 2}\n          fill=\"none\"\n          pathLength=\"100\"\n          r={selectedGeometry.trackRadius}\n          strokeDasharray={`${visibleTrack} ${100 - visibleTrack}`}\n          strokeDashoffset={trackOffset}\n          strokeLinecap=\"round\"\n          strokeWidth={thickness}\n        />\n        {percent === null || percent > 0 ? (\n          <path\n            data-slot=\"circular-progress-indicator\"\n            className={cn(\n              \"stroke-m3-primary transition-[stroke-dasharray] duration-(--m3-spring-effects-default-duration) ease-(--m3-spring-effects-default)\",\n              disabled && \"stroke-m3-on-surface/38\",\n              percent === null && \"m3-circular-progress-indeterminate\",\n            )}\n            d={selectedGeometry.indicatorPath}\n            fill=\"none\"\n            pathLength=\"100\"\n            strokeDasharray={\n              percent === null ? \"25 75\" : `${percent} ${100 - percent}`\n            }\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth={thickness}\n          />\n        ) : null}\n      </svg>\n    </ProgressPrimitive.Root>\n  )\n}\n\nexport { CircularProgress }\nexport type {\n  CircularProgressProps,\n  CircularProgressThickness,\n  CircularProgressVariant,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "feedback"
  ],
  "type": "registry:ui"
}