'use client'; import * as React from 'react'; import { cva, type VariantProps } from '@/lib/cva'; import * as ProgressPrimitive from 'radix-ui/progress'; import { cn } from '@/lib/utils'; import { colorPalettes } from '@/lib/cva-presets'; /* * CBAR's ring sizes and their stroke weights. The weight is not derivable from * the box — it climbs more slowly than the diameter does — so both are listed. */ const RING = { xs: { box: 24, stroke: 4 }, sm: { box: 32, stroke: 5 }, md: { box: 40, stroke: 6 }, lg: { box: 48, stroke: 7 }, xl: { box: 64, stroke: 8 }, } as const; export type ProgressCircleSize = keyof typeof RING; const progressCircleVariants = cva('relative inline-grid shrink-0 place-items-center', { variants: { size: { xs: 'size-6 text-xs', sm: 'size-8 text-xs', md: 'size-10 text-xs', lg: 'size-12 text-sm font-medium', xl: 'size-16 text-sm font-medium', }, colorPalette: colorPalettes, }, defaultVariants: { size: 'md', colorPalette: 'primary', }, }); export interface ProgressCircleProps extends Omit, 'children'>, VariantProps { /** * Print the percentage in the middle. * * Defaults to on from `md` up and off below it: `xs` and `sm` leave 16px and * 22px of clear space inside the ring, and "100%" at CBAR's 12px needs about * 30px, so the label spills over its own stroke. Pass `true` to force it — * with a `formatValue` that returns something short enough to fit. */ showValue?: boolean; /** Overrides the printed label. Receives the clamped 0–100 value. */ formatValue?: (value: number) => React.ReactNode; } /** * Circular progress (0–100). * * The linear {@link Progress} is the better default — a ring is for places * where a bar does not fit, like a tile corner or a table cell. Omit `value` * for an indeterminate task: Radix reports it as such and the ring spins. * * ```tsx * * ``` */ function ProgressCircle({ className, value, size = 'md', colorPalette, showValue, formatValue, ...props }: ProgressCircleProps) { const { box, stroke } = RING[size ?? 'md']; const withLabel = showValue ?? !(size === 'xs' || size === 'sm'); /* Stroke is centred on the path, so the radius has to come in by half of it or the ring is clipped by the viewBox. */ const radius = (box - stroke) / 2; const circumference = 2 * Math.PI * radius; const indeterminate = value === null || value === undefined; const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value)); return ( {withLabel && !indeterminate ? ( {formatValue ? formatValue(clamped) : `${Math.round(clamped)}%`} ) : null} ); } export { ProgressCircle, progressCircleVariants };