{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress",
  "title": "Progress",
  "author": "AlgorizeTech",
  "description": "A themeable progress bar in three forms — a single bar, a multi-segment track, and a ring — with an optional labelled field wrapper. Use it for determinate operations where percentage complete is known.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ProgressBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AlertCircle, AlertTriangle, CheckCircle2 } from \"lucide-react\";\nimport React, { forwardRef } from \"react\";\nimport { getThemeStyles } from \"./progress-theme\";\nimport { useCountUp } from \"./progress-hooks\";\n\n// ==========================================\n// VARIANT & COLOR CONFIGURATION\n// ==========================================\nexport type ProgressVariant = \"default\" | \"glass\" | \"glow\" | \"minimal\";\nexport type ProgressSize = \"sm\" | \"md\" | \"lg\";\nexport type ProgressColor =\n  | \"indigo\"\n  | \"rose\"\n  | \"emerald\"\n  | \"amber\"\n  | \"red\"\n  | \"zinc\"\n  | (string & {});\n\n/**\n * How the operation is going. `active` is the default and draws the theme\n * colour (or the current stage); the other three override every colour with\n * the conventional semantic hue so state is never ambiguous.\n */\nexport type ProgressStatus = \"active\" | \"success\" | \"warning\" | \"error\";\n\n/** A colour that takes over once the bar reaches `from` percent. */\nexport interface ProgressStage {\n  /** Threshold in percent (0–100), inclusive. */\n  from: number;\n  color: ProgressColor;\n}\n\nconst STATUS_COLOR: Record<Exclude<ProgressStatus, \"active\">, ProgressColor> = {\n  success: \"emerald\",\n  warning: \"amber\",\n  error: \"red\",\n};\n\n/**\n * The colour to draw right now. `status` wins outright; otherwise the last\n * stage whose threshold has been crossed; otherwise the base colour.\n */\nconst resolveProgressColor = (\n  percentage: number,\n  color: ProgressColor,\n  stages?: ProgressStage[],\n  status: ProgressStatus = \"active\",\n): ProgressColor => {\n  if (status !== \"active\") return STATUS_COLOR[status];\n  if (!stages?.length) return color;\n  let picked = color;\n  for (const stage of [...stages].sort((a, b) => a.from - b.from)) {\n    if (percentage >= stage.from) picked = stage.color;\n  }\n  return picked;\n};\n\n/** The readout's colour and glyph per status, shared by field and ring. */\nconst STATUS_READOUT: Record<\n  Exclude<ProgressStatus, \"active\">,\n  { text: string; Icon: React.ElementType }\n> = {\n  success: { text: \"text-emerald-600 dark:text-emerald-400\", Icon: CheckCircle2 },\n  warning: { text: \"text-amber-600 dark:text-amber-400\", Icon: AlertTriangle },\n  error: { text: \"text-red-600 dark:text-red-400\", Icon: AlertCircle },\n};\n\n// ==========================================\n// COUNT-UP ANIMATION (the tween itself, useCountUp, is in progress-hooks.ts)\n// ==========================================\n\n/** Decimal places in `n`, so a tweened value can be shown at the caller's precision. */\nconst decimalsOf = (n: number) => {\n  if (!Number.isFinite(n) || Number.isInteger(n)) return 0;\n  const text = String(n);\n  const dot = text.indexOf(\".\");\n  return dot < 0 ? 0 : Math.min(text.length - dot - 1, 6);\n};\n\nconst roundTo = (n: number, decimals: number) => {\n  const factor = 10 ** decimals;\n  return Math.round(n * factor) / factor;\n};\n\n/** The hover bubble shared by the bar and the ring. Positioned by the caller. */\nfunction ProgressTooltip({\n  children,\n  className,\n  style,\n}: {\n  children: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n}) {\n  return (\n    <div\n      role=\"tooltip\"\n      className={cn(\n        \"pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 whitespace-nowrap\",\n        \"rounded-md bg-foreground px-2 py-1 text-[11px] font-semibold tabular-nums text-background shadow-md\",\n        \"opacity-0 translate-y-1 transition-[opacity,transform,left] duration-200 ease-out\",\n        \"group-hover/progress:opacity-100 group-hover/progress:translate-y-0\",\n        // Caret, pointing down at the element.\n        \"after:absolute after:left-1/2 after:top-full after:-translate-x-1/2 after:border-4 after:border-transparent after:border-t-foreground\",\n        className,\n      )}\n      style={style}\n    >\n      {children}\n    </div>\n  );\n}\n\n// Keyframes for the indeterminate bar, injected once alongside the first bar\n// that needs them so the block stays self-contained when installed.\nconst INDETERMINATE_KEYFRAMES = `\n@keyframes progress-block-indeterminate {\n  0% { transform: translateX(-100%); }\n  100% { transform: translateX(260%); }\n}`;\n\n// ==========================================\n// SHARED TRACK GEOMETRY\n// ==========================================\n\n/** Bar height per size step. Shared by Progress and MultiProgress. */\nconst barSizeClasses: Record<ProgressSize, string> = {\n  sm: \"h-1.5\",\n  md: \"h-2.5\",\n  lg: \"h-4\",\n};\n\n/** Ring thickness in px per size step, so the circle reads at the same weight. */\nconst ringSizePx: Record<ProgressSize, number> = {\n  sm: 5,\n  md: 9,\n  lg: 14,\n};\n\n/**\n * Extra room given to the <svg> canvas for the glow variant's halo.\n *\n * An <svg> clips its own children (`overflow: hidden` in the UA stylesheet), so\n * a glow whose blur reaches the boundary gets sliced flat — measured, a 6px\n * drop-shadow paints ~11.4px past the stroke. Rather than shrink the ring to\n * fit, the canvas is grown by this much and offset back into place, so the ring\n * keeps its full `circleSize` and the halo spills past the box like a\n * box-shadow. Only the glow variant pays for it.\n */\nconst GLOW_BLEED = 14;\n\n/** The unfilled track, per variant. The fill is theme-coloured separately. */\nconst trackStyles: Record<ProgressVariant, string> = {\n  default: \"bg-muted border-none\",\n  glass:\n    \"bg-foreground/5 backdrop-blur-md border border-foreground/10 shadow-inner\",\n  glow: \"bg-muted border-none\",\n  minimal: \"bg-transparent border border-border\",\n};\n\n// ==========================================\n// PROGRESS FIELD WRAPPER\n// ==========================================\nexport interface ProgressFieldProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  label?: React.ReactNode;\n  value?: number;\n  max?: number;\n  showValue?: boolean;\n  valueFormat?: (value: number, max: number) => React.ReactNode;\n  /** Colours the readout and adds a glyph: a check on success, an alert on error. */\n  status?: ProgressStatus;\n  /** One line under the bar — what happened, or what to do next. */\n  hint?: React.ReactNode;\n  /** Count the readout up from zero on mount and tween every later change. */\n  animate?: boolean;\n  /** Length of the mount count-up in ms. Match the bar's `duration`. */\n  duration?: number;\n}\n\nexport const ProgressField = forwardRef<HTMLDivElement, ProgressFieldProps>(\n  (\n    {\n      label,\n      value = 0,\n      max = 100,\n      showValue = true,\n      valueFormat,\n      status = \"active\",\n      hint,\n      animate = true,\n      duration = 1200,\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    // The readout follows the same tween as the bar underneath, rounded back\n    // to the precision the caller passed so \"12 GB\" never becomes \"11.7 GB\".\n    const shown = useCountUp(value, { duration, enabled: animate, range: max });\n    const shownValue = roundTo(shown, decimalsOf(value));\n    const percentage = Math.min(Math.max((shown / max) * 100, 0), 100);\n    const readout = status !== \"active\" ? STATUS_READOUT[status] : null;\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex flex-col gap-2 w-full\", className)}\n        {...props}\n      >\n        {(label || showValue) && (\n          <div className=\"flex justify-between items-end gap-3 w-full\">\n            {label && (\n              <label className=\"text-sm font-semibold text-foreground\">\n                {label}\n              </label>\n            )}\n            {showValue && (\n              <span\n                className={cn(\n                  \"inline-flex items-center gap-1.5 text-sm font-semibold tabular-nums transition-colors\",\n                  readout ? readout.text : \"text-foreground\",\n                )}\n              >\n                {readout && <readout.Icon aria-hidden className=\"w-4 h-4\" />}\n                {valueFormat\n                  ? valueFormat(shownValue, max)\n                  : `${Math.round(percentage)}%`}\n              </span>\n            )}\n          </div>\n        )}\n        {children}\n        {hint && (\n          <p\n            className={cn(\n              \"text-xs transition-colors\",\n              readout ? readout.text : \"text-muted-foreground\",\n            )}\n          >\n            {hint}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nProgressField.displayName = \"ProgressField\";\n\n// ==========================================\n// PROGRESS BAR COMPONENT\n// ==========================================\nexport interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n  value?: number;\n  max?: number;\n  variant?: ProgressVariant;\n  color?: ProgressColor;\n  barSize?: ProgressSize;\n  indicatorClassName?: string;\n  /**\n   * Colour by threshold. Each stage takes over from its `from` percent until\n   * the next one, e.g. neutral to start, the brand colour once under way,\n   * green at 100. Overridden by `status`.\n   */\n  stages?: ProgressStage[];\n  /** Semantic override: success, warning or error colour the whole bar. */\n  status?: ProgressStatus;\n  /** Unknown duration — a bar slides along the track instead of filling it. */\n  indeterminate?: boolean;\n  /**\n   * Hover readout at the fill's leading edge. `true` shows the percentage;\n   * a function formats it, e.g. `(v, m) => v + \" of \" + m + \" GB\"`.\n   */\n  tooltip?: boolean | ((value: number, max: number) => React.ReactNode);\n  /**\n   * Fill from zero on mount and tween every later change of `value`, so the\n   * bar is never seen jumping. Stage colours are crossed on the way up.\n   */\n  animate?: boolean;\n  /** Length of the mount fill in ms; later changes take a proportional slice. */\n  duration?: number;\n}\n\nexport const Progress = forwardRef<HTMLDivElement, ProgressProps>(\n  (\n    {\n      value = 0,\n      max = 100,\n      variant = \"default\",\n      color = \"primary\",\n      barSize = \"md\",\n      stages,\n      status = \"active\",\n      indeterminate = false,\n      tooltip = false,\n      animate = true,\n      duration = 1200,\n      className,\n      indicatorClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    // Everything visual — width, stage colour, the tooltip — reads the tweened\n    // value; the ARIA attributes below report the real one.\n    const shown = useCountUp(value, {\n      duration,\n      enabled: animate && !indeterminate,\n      range: max,\n    });\n    const percentage = Math.min(Math.max((shown / max) * 100, 0), 100);\n    const activeColor = resolveProgressColor(percentage, color, stages, status);\n    const { theme, style } = getThemeStyles(activeColor as string);\n    const tooltipContent =\n      tooltip && !indeterminate\n        ? typeof tooltip === \"function\"\n          ? tooltip(roundTo(shown, decimalsOf(value)), max)\n          : `${Math.round(percentage)}%`\n        : null;\n\n    const indicatorStyles: Record<ProgressVariant, string> = {\n      default: theme.bg,\n      glass: cn(theme.bg, \"backdrop-blur-xl\"),\n      glow: cn(theme.bg, theme.glow),\n      minimal: theme.bg,\n    };\n\n    return (\n      <div\n        ref={ref}\n        role=\"progressbar\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={indeterminate ? undefined : value}\n        aria-valuetext={\n          indeterminate\n            ? \"In progress\"\n            : status !== \"active\"\n              ? `${Math.round(percentage)}%, ${status}`\n              : undefined\n        }\n        aria-busy={indeterminate || undefined}\n        data-status={status}\n        className={cn(\n          // The track itself does not clip: the fill is width-driven and\n          // rounded on its own, and the hover tooltip has to hang above the\n          // bar. The inner layer clips the sliding indeterminate bar instead.\n          \"group/progress relative w-full rounded-full\",\n          barSizeClasses[barSize],\n          trackStyles[variant],\n          className,\n        )}\n        style={style}\n        {...props}\n      >\n        {indeterminate && <style>{INDETERMINATE_KEYFRAMES}</style>}\n        <div className=\"absolute inset-0 overflow-hidden rounded-full\">\n          <div\n            className={cn(\n              // Width-driven so the fill's rounded end is real, and colour\n              // transitions alongside so a stage change cross-fades. When the\n              // tween drives the width there is nothing for CSS to smooth —\n              // and a transition on top would only lag behind it. The inset\n              // highlight is a hairline of light along the top edge — depth\n              // without a gradient wash.\n              \"h-full rounded-full duration-500 ease-out\",\n              animate\n                ? \"transition-[background-color,box-shadow]\"\n                : \"transition-[width,background-color,box-shadow]\",\n              \"shadow-[inset_0_1px_0_rgba(255,255,255,0.25)]\",\n              indicatorStyles[variant],\n              indeterminate &&\n                \"w-2/5 motion-safe:animate-[progress-block-indeterminate_1.4s_cubic-bezier(0.4,0,0.2,1)_infinite] motion-reduce:w-full\",\n              indicatorClassName,\n            )}\n            style={indeterminate ? undefined : { width: `${percentage}%` }}\n          />\n        </div>\n        {tooltipContent !== null && (\n          // Follows the fill's edge, held just inside the track at both ends\n          // so the bubble never hangs off the bar.\n          <ProgressTooltip\n            style={{ left: `clamp(1.25rem, ${percentage}%, calc(100% - 1.25rem))` }}\n          >\n            {tooltipContent}\n          </ProgressTooltip>\n        )}\n      </div>\n    );\n  },\n);\nProgress.displayName = \"Progress\";\n\n// ==========================================\n// MULTI (STACKED) PROGRESS\n// ==========================================\n\ninterface MultiProgressContextValue {\n  max: number;\n  gap: number;\n  animate: boolean;\n  duration: number;\n}\n\nconst MultiProgressContext =\n  React.createContext<MultiProgressContextValue | null>(null);\n\nconst useMultiProgress = () => {\n  const context = React.useContext(MultiProgressContext);\n  if (!context)\n    throw new Error(\n      \"<ProgressSegment /> must be rendered inside a <MultiProgress />.\",\n    );\n  return context;\n};\n\nexport interface MultiProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Shared denominator. Every segment's value is measured against this. */\n  max?: number;\n  variant?: ProgressVariant;\n  barSize?: ProgressSize;\n  /** Pixels between segments. 0 fuses them into one continuous bar. */\n  gap?: number;\n  /** Grow every segment from zero on mount and tween later changes. */\n  animate?: boolean;\n  /** Length of the mount fill in ms, shared by all segments. */\n  duration?: number;\n}\n\n/**\n * A single track holding several values at once — storage by file type, a\n * budget split across categories, votes across options.\n *\n * Segments are children rather than a `segments` array, so whatever shape your\n * data already has can be mapped straight into JSX without reshaping it first.\n */\nexport const MultiProgress = forwardRef<HTMLDivElement, MultiProgressProps>(\n  (\n    {\n      max = 100,\n      variant = \"default\",\n      barSize = \"md\",\n      gap = 0,\n      animate = true,\n      duration = 1200,\n      className,\n      style,\n      children,\n      ...props\n    },\n    ref,\n  ) => (\n    <MultiProgressContext.Provider value={{ max, gap, animate, duration }}>\n      <div\n        ref={ref}\n        role=\"group\"\n        className={cn(\n          \"relative flex w-full overflow-hidden rounded-full\",\n          barSizeClasses[barSize],\n          trackStyles[variant],\n          className,\n        )}\n        style={{ gap: gap ? `${gap}px` : undefined, ...style }}\n        {...props}\n      >\n        {children}\n      </div>\n    </MultiProgressContext.Provider>\n  ),\n);\nMultiProgress.displayName = \"MultiProgress\";\n\nexport interface ProgressSegmentProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  value?: number;\n  /** Named theme or a hex code — each segment carries its own colour. */\n  color?: ProgressColor;\n  /** Accessible name for the slice, and its native hover tooltip. */\n  label?: string;\n}\n\nexport const ProgressSegment = forwardRef<HTMLDivElement, ProgressSegmentProps>(\n  ({ value = 0, color = \"primary\", label, className, style, ...props }, ref) => {\n    const { max, gap, animate, duration } = useMultiProgress();\n    const { theme, style: themeStyle } = getThemeStyles(color as string);\n    // Every segment runs the same tween on the same clock, so the whole\n    // breakdown grows in proportion rather than slice by slice.\n    const shown = useCountUp(value, { duration, enabled: animate, range: max });\n    const percentage = Math.min(Math.max((shown / max) * 100, 0), 100);\n\n    return (\n      <div\n        ref={ref}\n        role=\"progressbar\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        aria-label={label}\n        title={label}\n        className={cn(\n          \"h-full duration-500 ease-out\",\n          animate ? \"transition-colors\" : \"transition-all\",\n          // Rounded ends only make sense once the segments are pulled apart.\n          gap > 0 && \"rounded-full\",\n          theme.bg,\n          className,\n        )}\n        style={{ width: `${percentage}%`, ...themeStyle, ...style }}\n        {...props}\n      />\n    );\n  },\n);\nProgressSegment.displayName = \"ProgressSegment\";\n\n// ==========================================\n// CIRCULAR (ROUND) PROGRESS\n// ==========================================\n\nexport interface CircularProgressProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  value?: number;\n  max?: number;\n  variant?: ProgressVariant;\n  color?: ProgressColor;\n  /** Ring thickness, on the same sm | md | lg scale as the bar. */\n  barSize?: ProgressSize;\n  /** Outer diameter of the ring in pixels. A glow halo fades out past it. */\n  circleSize?: number;\n  /** Draws the percentage in the middle. Ignored when children are supplied. */\n  showValue?: boolean;\n  valueFormat?: (value: number, max: number) => React.ReactNode;\n  /** Classes for the unfilled ring underneath. */\n  trackClassName?: string;\n  /** Classes for the filled arc. */\n  indicatorClassName?: string;\n  /** Colour by threshold — see `Progress`. */\n  stages?: ProgressStage[];\n  /** Semantic override; also colours the centred readout. */\n  status?: ProgressStatus;\n  /** Hover bubble above the ring — same options as on `Progress`. */\n  tooltip?: boolean | ((value: number, max: number) => React.ReactNode);\n  /** Sweep the arc from zero on mount and tween every later change. */\n  animate?: boolean;\n  /** Length of the mount sweep in ms; later changes take a proportional slice. */\n  duration?: number;\n  /**\n   * Replaces the centred readout. Pass a function to receive the value as it\n   * counts up, so a custom centre animates in step with the arc.\n   */\n  children?:\n    | React.ReactNode\n    | ((value: number, max: number) => React.ReactNode);\n}\n\n/**\n * The same value, drawn as a ring. Children replace the centred readout, so\n * the middle can hold an icon, a count, or a label instead of a percentage —\n * as a render function, they get the counting value to draw themselves.\n */\nexport const CircularProgress = forwardRef<\n  HTMLDivElement,\n  CircularProgressProps\n>(\n  (\n    {\n      value = 0,\n      max = 100,\n      variant = \"default\",\n      color = \"primary\",\n      barSize = \"md\",\n      circleSize = 120,\n      showValue = true,\n      valueFormat,\n      className,\n      trackClassName,\n      indicatorClassName,\n      stages,\n      status = \"active\",\n      tooltip = false,\n      animate = true,\n      duration = 1200,\n      style,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    // Arc, readout and tooltip all read the tweened value; ARIA the real one.\n    const shown = useCountUp(value, { duration, enabled: animate, range: max });\n    const shownValue = roundTo(shown, decimalsOf(value));\n    const percentage = Math.min(Math.max((shown / max) * 100, 0), 100);\n    const activeColor = resolveProgressColor(percentage, color, stages, status);\n    const { theme, style: themeStyle } = getThemeStyles(activeColor as string);\n    const readoutTone = status !== \"active\" ? STATUS_READOUT[status] : null;\n    const tooltipContent = tooltip\n      ? typeof tooltip === \"function\"\n        ? tooltip(shownValue, max)\n        : `${Math.round(percentage)}%`\n      : null;\n\n    // Geometry: inset the radius by half the stroke so the ring's outer edge\n    // lands exactly on the box, then walk the dash offset from a full\n    // circumference down to 0. The canvas is grown past the box only when a\n    // glow needs somewhere to fade out into.\n    const thickness = ringSizePx[barSize];\n    const radius = Math.max((circleSize - thickness) / 2, 1);\n    const circumference = 2 * Math.PI * radius;\n    const bleed = variant === \"glow\" ? GLOW_BLEED : 0;\n    const canvas = circleSize + bleed * 2;\n    const center = canvas / 2;\n\n    const trackStroke: Record<ProgressVariant, string> = {\n      default: \"text-muted\",\n      glass: \"text-foreground/10\",\n      glow: \"text-muted\",\n      minimal: \"text-border\",\n    };\n\n    const readout =\n      typeof children === \"function\"\n        ? children(shownValue, max)\n        : (children ??\n          (showValue\n            ? valueFormat\n              ? valueFormat(shownValue, max)\n              : `${Math.round(percentage)}%`\n            : null));\n\n    return (\n      <div\n        ref={ref}\n        role=\"progressbar\"\n        aria-valuemin={0}\n        aria-valuemax={max}\n        aria-valuenow={value}\n        data-status={status}\n        className={cn(\n          \"group/progress relative inline-flex shrink-0 items-center justify-center\",\n          variant === \"glass\" && \"rounded-full backdrop-blur-md\",\n          className,\n        )}\n        style={{\n          width: circleSize,\n          height: circleSize,\n          ...themeStyle,\n          ...style,\n        }}\n        {...props}\n      >\n        <svg\n          width={canvas}\n          height={canvas}\n          viewBox={`0 0 ${canvas} ${canvas}`}\n          // Pulled back by the bleed so the ring stays centred on the box.\n          className=\"absolute max-w-none -rotate-90\"\n          style={{ left: -bleed, top: -bleed }}\n          aria-hidden=\"true\"\n        >\n          <circle\n            cx={center}\n            cy={center}\n            r={radius}\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={thickness}\n            className={cn(trackStroke[variant], trackClassName)}\n          />\n          <circle\n            cx={center}\n            cy={center}\n            r={radius}\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={thickness}\n            strokeLinecap=\"round\"\n            strokeDasharray={circumference}\n            strokeDashoffset={circumference * (1 - percentage / 100)}\n            className={cn(\n              \"duration-500 ease-out\",\n              animate ? \"transition-colors\" : \"transition-[stroke-dashoffset,color]\",\n              theme.stroke,\n              variant === \"glow\" && theme.ringGlow,\n              indicatorClassName,\n            )}\n          />\n        </svg>\n        {readout !== null && readout !== false && (\n          <div\n            className={cn(\n              \"absolute inset-0 flex flex-col items-center justify-center text-center font-semibold leading-tight tabular-nums transition-colors\",\n              readoutTone ? readoutTone.text : \"text-foreground\",\n            )}\n            style={{ fontSize: Math.max(12, Math.round(circleSize * 0.18)) }}\n          >\n            {readout}\n          </div>\n        )}\n        {tooltipContent !== null && (\n          <ProgressTooltip className=\"left-1/2\">{tooltipContent}</ProgressTooltip>\n        )}\n      </div>\n    );\n  },\n);\nCircularProgress.displayName = \"CircularProgress\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/ProgressBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/progress-hooks.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\n\n/** One easing for the fill and the readout, so they can never drift apart. */\nconst easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  if (typeof window === \"undefined\" || !window.matchMedia) return () => {};\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener?.(\"change\", onChange);\n  return () => mq.removeEventListener?.(\"change\", onChange);\n};\nconst prefersReducedMotion = () =>\n  typeof window !== \"undefined\" && window.matchMedia?.(REDUCED_MOTION).matches === true;\n/** The visitor's reduced-motion setting; false while server-rendering. */\nconst usePrefersReducedMotion = () =>\n  useSyncExternalStore(subscribeReducedMotion, prefersReducedMotion, () => false);\n\nexport interface CountUpOptions {\n  /** Length of the first run, from zero, in ms. Default 1200. */\n  duration?: number;\n  /** `false` returns `target` untouched. */\n  enabled?: boolean;\n  /**\n   * The scale later changes are measured against (the bar's `max`). A change\n   * across the whole range takes `duration`; a 1% tick takes a sliver of it.\n   */\n  range?: number;\n}\n\n/**\n * Tweens a number towards `target`. It starts at 0 on mount — so the bar\n * fills and the readout counts up when it first appears — then follows every\n * later change from wherever it currently is, so a stream of small updates\n * never jumps. The mount run always takes the full `duration`; later runs\n * take a slice proportional to how far they travel, floored so a tiny nudge\n * still visibly moves. Under `prefers-reduced-motion` it returns `target`.\n */\nexport function useCountUp(\n  target: number,\n  { duration = 1200, enabled = true, range }: CountUpOptions = {},\n): number {\n  const reduced = usePrefersReducedMotion();\n  const snap = !enabled || reduced;\n  const [current, setCurrent] = useState(enabled ? 0 : target);\n  // While snapping, the value sits on `target`, so a later run starts from it.\n  if (snap && current !== target) setCurrent(target);\n  const currentRef = useRef(current);\n  // Flipped by the first frame that actually moves, not by the effect — a\n  // run that is cancelled before it paints (strict mode) is not a mount run.\n  const movedRef = useRef(false);\n\n  useEffect(() => {\n    if (snap) {\n      currentRef.current = target;\n      return;\n    }\n    const from = currentRef.current;\n    const delta = target - from;\n    if (delta === 0) return;\n\n    const scale = Math.max(Math.abs(range ?? target), Math.abs(from), 1);\n    const ms = movedRef.current\n      ? Math.max(160, duration * Math.min(1, Math.abs(delta) / scale))\n      : duration;\n\n    let raf = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const t = Math.min(1, (now - start) / ms);\n      const next = t >= 1 ? target : from + delta * easeOutCubic(t);\n      currentRef.current = next;\n      movedRef.current = true;\n      setCurrent(next);\n      if (t < 1) raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [target, duration, snap, range]);\n\n  return snap ? target : current;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/progress-hooks.ts"
    },
    {
      "path": "src/components/custom/blocks/progress-theme.ts",
      "content": "\"use client\";\n\nimport React from \"react\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    bg: \"bg-primary\",\n    bgSoft: \"bg-primary/20\",\n    glow: \"shadow-[0_0_15px_var(--primary)]\",\n    // `stroke` and `ringGlow` are the SVG equivalents of `bg` and `glow`, used\n    // by CircularProgress — an SVG arc takes a stroke colour, not a background.\n    stroke: \"text-primary\",\n    ringGlow: \"drop-shadow-[0_0_6px_var(--primary)]\",\n  },\n  indigo: {\n    bg: \"bg-indigo-600\",\n    bgSoft: \"bg-indigo-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(99,102,241,0.5)]\",\n    stroke: \"text-indigo-600\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(99,102,241,0.7)]\",\n  },\n  rose: {\n    bg: \"bg-rose-600\",\n    bgSoft: \"bg-rose-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(244,63,94,0.5)]\",\n    stroke: \"text-rose-600\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(244,63,94,0.7)]\",\n  },\n  emerald: {\n    bg: \"bg-emerald-600\",\n    bgSoft: \"bg-emerald-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(16,185,129,0.5)]\",\n    stroke: \"text-emerald-600\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(16,185,129,0.7)]\",\n  },\n  // Semantic colours used by `status`. Deliberately the conventional hues —\n  // a failed upload should look failed on any brand.\n  amber: {\n    bg: \"bg-amber-500\",\n    bgSoft: \"bg-amber-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(245,158,11,0.5)]\",\n    stroke: \"text-amber-500\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(245,158,11,0.7)]\",\n  },\n  red: {\n    bg: \"bg-red-500\",\n    bgSoft: \"bg-red-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(239,68,68,0.5)]\",\n    stroke: \"text-red-500\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(239,68,68,0.7)]\",\n  },\n  zinc: {\n    bg: \"bg-zinc-900 dark:bg-zinc-100\",\n    bgSoft: \"bg-zinc-500/20\",\n    glow: \"shadow-[0_0_15px_rgba(255,255,255,0.3)] dark:shadow-[0_0_15px_rgba(255,255,255,0.4)]\",\n    stroke: \"text-zinc-900 dark:text-zinc-100\",\n    ringGlow: \"drop-shadow-[0_0_6px_rgba(161,161,170,0.7)]\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color: string) => {\n  if (color && color.startsWith(\"#\")) {\n    const rgb = hexToRgb(color);\n    return {\n      theme: {\n        bg: \"bg-[var(--progress-custom)]\",\n        bgSoft: \"bg-[var(--progress-custom-20)]\",\n        glow: \"shadow-[0_0_15px_var(--progress-custom-50)]\",\n        stroke: \"text-[var(--progress-custom)]\",\n        ringGlow: \"drop-shadow-[0_0_6px_var(--progress-custom-50)]\",\n      },\n      style: {\n        \"--progress-custom\": color,\n        \"--progress-custom-20\": `rgba(${rgb}, 0.2)`,\n        \"--progress-custom-50\": `rgba(${rgb}, 0.5)`,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n",
      "type": "registry:component",
      "target": "components/custom/blocks/progress-theme.ts"
    }
  ],
  "docs": "Named exports: Progress, ProgressField, MultiProgress, ProgressSegment, CircularProgress (getThemeStyles lives in progress-theme.ts, useCountUp in progress-hooks.ts; both install alongside). Use ProgressField when you need a label and value readout; use Progress bare inside your own layout. MultiProgress holds ProgressSegment children that share its `max`, each with its own `color`, for breakdowns across one track. CircularProgress draws the same value as a ring — `circleSize` is the diameter, `barSize` the stroke, and children replace the centred percentage (a function child receives the counting value). Everything fills from zero on mount and tweens later changes via the useCountUp hook; `animate={false}` opts out and `duration` sets the mount run.",
  "categories": [
    "feedback"
  ],
  "type": "registry:component"
}
