{
  "name": "progress-indicators",
  "layer": "base",
  "type": "component",
  "title": "Progress indicator components",
  "description": "Free and open-source React progress indicator components built for modern applications and websites. These progress indicators are built using React Aria and styled with Tailwind CSS.",
  "files": [
    {
      "path": "components/base/progress-indicators/progress-circles.tsx",
      "target": "components/base/progress-indicators/progress-circles.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { cx as clx, sortCx } from \"@/utils/cx\";\n\nexport interface ProgressBarProps {\n    /** The current value of the progress bar. */\n    value: number;\n    /** The minimum value of the progress bar. @default 0 */\n    min?: number;\n    /** The maximum value of the progress bar. @default 100 */\n    max?: number;\n    /** The size of the circular progress bar. */\n    size: \"xxs\" | \"xs\" | \"sm\" | \"md\" | \"lg\";\n    /** Optional label rendered above the value inside the circle. */\n    label?: string;\n    /** Optional function to format the displayed value. It receives the raw value and the calculated percentage. */\n    valueFormatter?: (value: number, valueInPercentage: number) => string | number;\n}\n\nconst sizes = sortCx({\n    xxs: {\n        strokeWidth: 6,\n        radius: 29,\n        valueClass: \"text-sm font-semibold text-primary\",\n        labelClass: \"text-xs font-medium text-tertiary\",\n        halfCircleTextPosition: \"absolute bottom-0.5 text-center\",\n    },\n    xs: {\n        strokeWidth: 16,\n        radius: 72,\n        valueClass: \"text-display-xs font-semibold text-primary\",\n        labelClass: \"text-xs font-medium text-tertiary\",\n        halfCircleTextPosition: \"absolute bottom-0.5 text-center\",\n    },\n    sm: {\n        strokeWidth: 20,\n        radius: 90,\n        valueClass: \"text-display-sm font-semibold text-primary\",\n        labelClass: \"text-xs font-medium text-tertiary\",\n        halfCircleTextPosition: \"absolute bottom-2 text-center\",\n    },\n    md: {\n        strokeWidth: 24,\n        radius: 108,\n        valueClass: \"text-display-md font-semibold text-primary\",\n        labelClass: \"text-sm font-medium text-tertiary\",\n        halfCircleTextPosition: \"absolute bottom-1 text-center\",\n    },\n    lg: {\n        strokeWidth: 28,\n        radius: 126,\n        valueClass: \"text-display-lg font-semibold text-primary\",\n        labelClass: \"text-sm font-medium text-tertiary\",\n        halfCircleTextPosition: \"absolute bottom-0 text-center\",\n    },\n});\n\nexport const ProgressBarCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {\n    const percentage = Math.round(((value - min) * 100) / (max - min));\n\n    const sizeConfig = sizes[size];\n\n    const { strokeWidth, radius, valueClass, labelClass } = sizeConfig;\n\n    const diameter = 2 * (radius + strokeWidth / 2);\n    const width = diameter;\n    const height = diameter;\n    const viewBox = `0 0 ${width} ${height}`;\n    const cx = diameter / 2;\n    const cy = diameter / 2;\n\n    const textPosition = label ? \"absolute text-center\" : \"absolute text-primary\";\n    const strokeDashoffset = 100 - percentage;\n\n    return (\n        <div className=\"flex flex-col items-center gap-0.5\">\n            <div\n                role=\"progressbar\"\n                aria-label={label ?? \"Progress\"}\n                aria-valuenow={value}\n                aria-valuemin={min}\n                aria-valuemax={max}\n                className=\"relative flex w-max items-center justify-center\"\n            >\n                <svg className=\"-rotate-90\" width={width} height={height} viewBox={viewBox}>\n                    {/* Background circle */}\n                    <circle\n                        className=\"stroke-bg-quaternary\"\n                        cx={cx}\n                        cy={cy}\n                        r={radius}\n                        fill=\"none\"\n                        strokeWidth={strokeWidth}\n                        pathLength=\"100\"\n                        strokeDasharray=\"100\"\n                        strokeLinecap=\"round\"\n                    />\n\n                    {/* Foreground circle */}\n                    <circle\n                        className=\"stroke-fg-brand-primary\"\n                        cx={cx}\n                        cy={cy}\n                        r={radius}\n                        fill=\"none\"\n                        strokeWidth={strokeWidth}\n                        pathLength=\"100\"\n                        strokeDasharray=\"100\"\n                        strokeLinecap=\"round\"\n                        strokeDashoffset={strokeDashoffset}\n                    />\n                </svg>\n                {label && size !== \"xxs\" ? (\n                    <div className=\"absolute text-center\">\n                        <div className={labelClass}>{label}</div>\n                        <div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>\n                    </div>\n                ) : (\n                    <span className={clx(textPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>\n                )}\n            </div>\n\n            {label && size === \"xxs\" && <div className={labelClass}>{label}</div>}\n        </div>\n    );\n};\n\nexport const ProgressBarHalfCircle = ({ value, min = 0, max = 100, size, label, valueFormatter }: ProgressBarProps) => {\n    const percentage = Math.round(((value - min) * 100) / (max - min));\n\n    const sizeConfig = sizes[size];\n\n    const { strokeWidth, radius, valueClass, labelClass, halfCircleTextPosition } = sizeConfig;\n\n    const width = 2 * (radius + strokeWidth / 2);\n    const height = radius + strokeWidth;\n    const viewBox = `0 0 ${width} ${height}`;\n    const cx = \"50%\";\n    const cy = radius + strokeWidth / 2;\n\n    const strokeDashoffset = -50 - (100 - percentage) / 2;\n\n    return (\n        <div className=\"flex flex-col items-center gap-0.5\">\n            <div\n                role=\"progressbar\"\n                aria-label={label ?? \"Progress\"}\n                aria-valuenow={value}\n                aria-valuemin={min}\n                aria-valuemax={max}\n                className=\"relative flex w-max items-center justify-center\"\n            >\n                <svg width={width} height={height} viewBox={viewBox}>\n                    {/* Background half-circle */}\n                    <circle\n                        className=\"stroke-bg-quaternary\"\n                        cx={cx}\n                        cy={cy}\n                        r={radius}\n                        fill=\"none\"\n                        strokeWidth={strokeWidth}\n                        pathLength=\"100\"\n                        strokeDasharray=\"100\"\n                        strokeDashoffset=\"-50\"\n                        strokeLinecap=\"round\"\n                    />\n\n                    {/* Foreground half-circle */}\n                    <circle\n                        className=\"stroke-fg-brand-primary origin-center -scale-x-100\"\n                        cx={cx}\n                        cy={cy}\n                        r={radius}\n                        fill=\"none\"\n                        strokeWidth={strokeWidth}\n                        pathLength=\"100\"\n                        strokeDasharray=\"100\"\n                        strokeDashoffset={strokeDashoffset}\n                        strokeLinecap=\"round\"\n                    />\n                </svg>\n\n                {label && size !== \"xxs\" ? (\n                    <div className={halfCircleTextPosition}>\n                        <div className={labelClass}>{label}</div>\n                        <div className={valueClass}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</div>\n                    </div>\n                ) : (\n                    <span className={clx(halfCircleTextPosition, valueClass)}>{valueFormatter ? valueFormatter(value, percentage) : `${percentage}%`}</span>\n                )}\n            </div>\n\n            {label && size === \"xxs\" && <div className={labelClass}>{label}</div>}\n        </div>\n    );\n};\n",
      "dependencies": []
    },
    {
      "path": "components/base/progress-indicators/progress-indicators.tsx",
      "target": "components/base/progress-indicators/progress-indicators.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { cx } from \"@/utils/cx\";\n\nexport interface ProgressBarProps {\n    /**\n     * The current value of the progress bar.\n     */\n    value: number;\n    /**\n     * The minimum value of the progress bar.\n     * @default 0\n     */\n    min?: number;\n    /**\n     * The maximum value of the progress bar.\n     * @default 100\n     */\n    max?: number;\n    /**\n     * Optional additional CSS class names for the progress bar container.\n     */\n    className?: string;\n    /**\n     * Optional additional CSS class names for the progress bar indicator element.\n     */\n    progressClassName?: string;\n    /**\n     * Optional function to format the displayed value.\n     * It receives the raw value and the calculated percentage.\n     */\n    valueFormatter?: (value: number, valueInPercentage: number) => string | number;\n}\n\n/**\n * A basic progress bar component.\n */\nexport const ProgressBarBase = ({ value, min = 0, max = 100, className, progressClassName }: ProgressBarProps) => {\n    const percentage = ((value - min) * 100) / (max - min);\n\n    return (\n        <div\n            role=\"progressbar\"\n            aria-label=\"Progress\"\n            aria-valuenow={value}\n            aria-valuemin={min}\n            aria-valuemax={max}\n            className={cx(\"bg-quaternary h-2 w-full overflow-hidden rounded-md\", className)}\n        >\n            <div\n                // Use transform instead of width to avoid layout thrashing (and for smoother animation)\n                style={{ transform: `translateX(-${100 - percentage}%)` }}\n                className={cx(\"bg-fg-brand-primary size-full rounded-md transition duration-75 ease-linear\", progressClassName)}\n            />\n        </div>\n    );\n};\n\ntype ProgressBarLabelPosition = \"right\" | \"bottom\" | \"top-floating\" | \"bottom-floating\";\n\nexport interface ProgressIndicatorWithTextProps extends ProgressBarProps {\n    /**\n     * Specifies the layout of the text relative to the progress bar.\n     * - `right`: Text is displayed to the right of the progress bar.\n     * - `bottom`: Text is displayed below the progress bar, aligned to the right.\n     * - `top-floating`: Text is displayed in a floating tooltip above the progress indicator.\n     * - `bottom-floating`: Text is displayed in a floating tooltip below the progress indicator.\n     */\n    labelPosition?: ProgressBarLabelPosition;\n}\n\n/**\n * A progress bar component that displays the value text in various configurable layouts.\n */\nexport const ProgressBar = ({ value, min = 0, max = 100, valueFormatter, labelPosition, className, progressClassName }: ProgressIndicatorWithTextProps) => {\n    const percentage = ((value - min) * 100) / (max - min);\n    const formattedValue = valueFormatter ? valueFormatter(value, percentage) : `${percentage.toFixed(0)}%`; // Default to rounded percentage\n\n    const baseProgressBar = <ProgressBarBase min={min} max={max} value={value} className={className} progressClassName={progressClassName} />;\n\n    switch (labelPosition) {\n        case \"right\":\n            return (\n                <div className=\"flex items-center gap-3\">\n                    {baseProgressBar}\n                    <span className=\"text-secondary shrink-0 text-sm font-medium tabular-nums\">{formattedValue}</span>\n                </div>\n            );\n        case \"bottom\":\n            return (\n                <div className=\"flex flex-col items-end gap-2\">\n                    {baseProgressBar}\n                    <span className=\"text-secondary text-sm font-medium tabular-nums\">{formattedValue}</span>\n                </div>\n            );\n        case \"top-floating\":\n            return (\n                <div className=\"relative flex flex-col items-end gap-2\">\n                    {baseProgressBar}\n                    <div\n                        style={{ left: `${percentage}%` }}\n                        className=\"bg-primary_alt ring-secondary_alt absolute -top-2 -translate-x-1/2 -translate-y-full rounded-lg px-3 py-2 shadow-lg ring-1\"\n                    >\n                        <div className=\"text-secondary text-xs font-semibold tabular-nums\">{formattedValue}</div>\n                    </div>\n                </div>\n            );\n        case \"bottom-floating\":\n            return (\n                <div className=\"relative flex flex-col items-end gap-2\">\n                    {baseProgressBar}\n                    <div\n                        style={{ left: `${percentage}%` }}\n                        className=\"bg-primary_alt ring-secondary_alt absolute -bottom-2 -translate-x-1/2 translate-y-full rounded-lg px-3 py-2 shadow-lg ring-1\"\n                    >\n                        <div className=\"text-secondary text-xs font-semibold\">{formattedValue}</div>\n                    </div>\n                </div>\n            );\n        default:\n            // Fallback or default case, could render the basic progress bar or throw an error\n            return baseProgressBar;\n    }\n};\n",
      "dependencies": []
    },
    {
      "path": "components/base/progress-indicators/simple-circle.tsx",
      "target": "components/base/progress-indicators/simple-circle.tsx",
      "type": "component",
      "content": "\"use client\";\n\nexport const CircleProgressBar = (props: { value: number; min?: 0; max?: 100 }) => {\n    const { value, min = 0, max = 100 } = props;\n    const percentage = ((value - min) * 100) / (max - min);\n\n    return (\n        <div role=\"progressbar\" aria-valuenow={value} aria-valuemin={min} aria-valuemax={max} className=\"relative flex w-max items-center justify-center\">\n            <span className=\"text-primary absolute text-sm font-medium\">{percentage}%</span>\n            <svg className=\"size-16 -rotate-90\" viewBox=\"0 0 60 60\">\n                <circle className=\"stroke-bg-quaternary\" cx=\"30\" cy=\"30\" r=\"26\" fill=\"none\" strokeWidth=\"6\" />\n                <circle\n                    className=\"stroke-fg-brand-primary\"\n                    style={{\n                        strokeDashoffset: `calc(100 - ${percentage})`,\n                    }}\n                    cx=\"30\"\n                    cy=\"30\"\n                    r=\"26\"\n                    fill=\"none\"\n                    strokeWidth=\"6\"\n                    strokeDasharray=\"100\"\n                    pathLength=\"100\"\n                    strokeLinecap=\"round\"\n                />\n            </svg>\n        </div>\n    );\n};\n",
      "dependencies": []
    }
  ],
  "registryDependencies": [
    "cx"
  ],
  "optionalRegistryDependencies": [],
  "dependencies": [],
  "cssVars": [],
  "examples": [
    "circle-progress-bar",
    "circle-progress-bar-label",
    "default",
    "half-circle-progress-bar",
    "half-circle-progress-bar-label",
    "progress-indicator-example",
    "text-bottom",
    "text-bottom-floating",
    "text-right",
    "text-top-floating"
  ],
  "docs": "/components/progress-indicators",
  "intent": "Show how far a determinate, ongoing process (upload, multi-step task, loading) has progressed.",
  "avoid_when": [
    "the process has no known end point or percentage (use loading-indicator)"
  ],
  "composes_with": [
    "file-upload",
    "progress-steps",
    "metrics"
  ],
  "a11y_contract": [
    "renders `role=\"progressbar\"` with aria-valuenow/min/max wired to the value/min/max props automatically; a default aria-label of \"Progress\" applies unless the caller supplies `label`"
  ],
  "requires_data": [
    "a numeric `value` (and optionally min/max) representing real progress"
  ],
  "token_contract": [
    "bg-fg-brand-primary",
    "bg-primary_alt",
    "bg-quaternary",
    "text-primary",
    "text-secondary",
    "text-tertiary"
  ],
  "changelog": [],
  "platforms": [
    "react",
    "next"
  ]
}
