{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"progress-circle","type":"registry:ui","title":"Progress Circle","description":"Progress as a ring, for a small space: a file in a list, a quota on a card.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-count-to.json","https://dashral.com/r/lib-locale.json"],"files":[{"path":"registry/dashral/progress-circle.tsx","type":"registry:file","target":"components/dashral/progress-circle.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Progress as ProgressPrimitive } from \"radix-ui\"\n\nimport { CountTo } from \"./lib/count-to\"\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\n\ntype ControlColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\n// The ring's color; the track behind it is a pale version. Every class is written out in full so Tailwind finds it.\nconst percentOptions: Intl.NumberFormatOptions = { style: \"percent\" }\n\nconst ringColors: Record<ControlColor, string> = {\n  primary: \"text-primary\",\n  neutral: \"text-foreground\",\n  info: \"text-info\",\n  success: \"text-success\",\n  warning: \"text-warning\",\n  destructive: \"text-destructive\",\n  highlight: \"text-highlight\",\n}\n\ntype ProgressCircleProps = Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, \"color\" | \"children\"> & {\n  /** 0–100; null (or left out) for indeterminate: an arc that spins. */\n  value?: number | null\n  /** The ring's color; the track is a pale version of it. */\n  color?: ControlColor\n  /** Width and height in pixels. */\n  size?: number\n  /** The ring's thickness in pixels. */\n  thickness?: number\n  /** The percentage in the middle, written in the locale (\"64%\", \"٦٤٪\"). */\n  showValue?: boolean\n  /** Anything in the middle instead, e.g. an icon or \"3/5\". */\n  children?: React.ReactNode\n  classNames?: { track?: string; indicator?: string; value?: string }\n}\n\n/**\n * Progress as a ring, for a small space: a file's upload in a list, a step in a checklist, a quota on a card. The same\n * progressbar as Progress for assistive technology; label it with aria-label.\n */\nfunction ProgressCircle({\n  value = null,\n  color = \"primary\",\n  size = 40,\n  thickness = 4,\n  showValue = false,\n  children,\n  className,\n  classNames,\n  style,\n  ...props\n}: ProgressCircleProps) {\n  const { locale } = useLocale()\n  // Starts empty and fills to its value once on screen, as Gauge does.\n  const [drawn, setDrawn] = React.useState(false)\n  React.useEffect(() => setDrawn(true), [])\n  const radius = (size - thickness) / 2\n  const circumference = 2 * Math.PI * radius\n  const indeterminate = value === null || value === undefined\n  const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value))\n  // Indeterminate: a quarter of the ring, spinning.\n  const offset = indeterminate ? circumference * 0.75 : circumference * (1 - (drawn ? clamped : 0) / 100)\n  const center = children ?? (showValue && !indeterminate ? <CountTo value={clamped / 100} locale={locale} formatOptions={percentOptions} /> : null)\n\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress-circle\"\n      value={indeterminate ? null : clamped}\n      className={cn(\"relative inline-grid shrink-0 place-items-center\", ringColors[color], className)}\n      style={{ width: size, height: size, ...style }}\n      {...props}\n    >\n      <svg\n        aria-hidden\n        width={size}\n        height={size}\n        viewBox={`0 0 ${size} ${size}`}\n        className={cn(\n          \"col-start-1 row-start-1 -rotate-90\",\n          indeterminate && \"animate-spin motion-reduce:animate-[spin_1.5s_linear_infinite]\"\n        )}\n      >\n        <circle\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={thickness}\n          className={cn(\"opacity-15\", classNames?.track)}\n        />\n        <circle\n          data-slot=\"progress-circle-indicator\"\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={thickness}\n          strokeLinecap=\"round\"\n          strokeDasharray={circumference}\n          strokeDashoffset={offset}\n          // A zero-length round cap still draws a dot; hide it at 0.\n          opacity={!indeterminate && (!drawn || clamped === 0) ? 0 : 1}\n          className={cn(\"transition-[stroke-dashoffset] duration-(--duration-considered) ease-enter motion-reduce:transition-none\", classNames?.indicator)}\n        />\n      </svg>\n      {center !== null && (\n        <span\n          className={cn(\n            \"col-start-1 row-start-1 text-foreground tabular-nums\",\n            size < 48 ? \"text-[10px] font-medium\" : size < 80 ? \"text-xs font-medium\" : \"text-base font-semibold\",\n            classNames?.value\n          )}\n        >\n          {center}\n        </span>\n      )}\n    </ProgressPrimitive.Root>\n  )\n}\n\nexport { ProgressCircle, type ProgressCircleProps }\n"}],"cssVars":{"theme":{"color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}