{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress-button",
  "type": "registry:ui",
  "title": "Progress Button",
  "description": "A download button whose fill sweeps across as work progresses, with pause, cancel, retry and a rolling percentage.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/progress-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useTransform } from \"motion/react\";\n\nexport type ProgressButtonStatus = \"idle\" | \"running\" | \"paused\" | \"complete\" | \"error\";\n\n/**\n * Reports progress (0–1) from an `onStart` task. The returned promise stays pending while the user has\n * paused, and rejects once they cancel — `await` it inside your loop to honour pause and cancel.\n */\nexport type ProgressReport = (progress: number) => Promise<void>;\n\ntype Spring = { stiffness: number; damping: number; mass: number };\n\nexport interface ProgressButtonProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"button\">,\n    | \"children\"\n    | \"onDrag\"\n    | \"onDragStart\"\n    | \"onDragEnd\"\n    | \"onAnimationStart\"\n    | \"onAnimationEnd\"\n    | \"onPause\"\n    | \"onCancel\"\n    | \"onError\"\n  > {\n  /** Idle label, also the base of the accessible names (\"Pause Download\", \"Cancel Download\"). */\n  label?: string;\n  /** Visible label while running. */\n  runningLabel?: string;\n  /** Visible label while paused. */\n  pausedLabel?: string;\n  /** Visible and announced label once complete. */\n  completeLabel?: string;\n  /** Visible and announced label on failure (the button retries when pressed). */\n  errorLabel?: string;\n  /** Controlled progress, 0–1. */\n  progress?: number;\n  /**\n   * Controlled status. When set, presses only *request* a new status through `onStatusChange`;\n   * you drive `progress` and `status` yourself and `onStart` is not called.\n   */\n  status?: ProgressButtonStatus;\n  /**\n   * Uncontrolled mode: the task to run on press (and on retry). Call `report(0..1)` as it advances;\n   * resolve to complete, reject to fail. `signal` aborts when the user cancels or the button unmounts.\n   */\n  onStart?: (report: ProgressReport, signal: AbortSignal) => Promise<unknown> | unknown;\n  /** Called on every status change (uncontrolled) or status request (controlled). */\n  onStatusChange?: (status: ProgressButtonStatus) => void;\n  onPause?: () => void;\n  onResume?: () => void;\n  onCancel?: () => void;\n  onComplete?: () => void;\n  /** Receives the rejection reason when `onStart` fails. */\n  onError?: (error: unknown) => void;\n  /** Allow pausing by pressing the button while it runs. */\n  pausable?: boolean;\n  /** Show a cancel button (and let Escape cancel) while running or paused. */\n  cancellable?: boolean;\n  /** Time in ms before \"complete\" returns to idle (uncontrolled). 0 keeps it complete. */\n  resetDelay?: number;\n  /** `fill` sweeps a solid fill across the button; `bar` draws a hairline progress bar. */\n  variant?: \"fill\" | \"bar\";\n  /** Fill, bar and focus-ring colour. */\n  accent?: string;\n  /** Text colour on top of the fill. */\n  inkColor?: string;\n  /** Fill colour in the error state. */\n  errorColor?: string;\n  /** Spring that eases the fill towards each reported value. */\n  spring?: Spring;\n  /** Width of the main button (any CSS length). It shrinks to fit narrow containers. */\n  width?: number | string;\n  /** Optional icon shown before the idle label (replaces the download glyph). */\n  icon?: React.ReactNode;\n}\n\n/* --------------------------------- helpers --------------------------------- */\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nconst clamp01 = (n: number) => (Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0);\nconst noop = () => {};\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\ninterface Gate {\n  promise: Promise<void>;\n  resolve: () => void;\n  reject: (reason: unknown) => void;\n}\n\n/** A promise that `report()` hands out while paused. */\nfunction createGate(): Gate {\n  let resolve: () => void = noop;\n  let reject: (reason: unknown) => void = noop;\n  const promise = new Promise<void>((res, rej) => {\n    resolve = res;\n    reject = rej;\n  });\n  // Mark as handled: callers that never await report() must not see an unhandled rejection on cancel.\n  promise.catch(noop);\n  return { promise, resolve, reject };\n}\n\n/* ---------------------------------- glyphs --------------------------------- */\n\nconst glyph = \"size-4 shrink-0\";\n\nfunction Glyph({ kind, instant }: { kind: \"download\" | \"pause\" | \"play\" | \"check\" | \"retry\" | \"close\"; instant?: boolean }) {\n  const common = { fill: \"none\", stroke: \"currentColor\", strokeWidth: 1.75, strokeLinecap: \"round\", strokeLinejoin: \"round\" } as const;\n  switch (kind) {\n    case \"download\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className={glyph} aria-hidden=\"true\" {...common}>\n          <path d=\"M8 2.5v7.5M4.75 7l3.25 3.25L11.25 7M3 13h10\" />\n        </svg>\n      );\n    case \"pause\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className={glyph} aria-hidden=\"true\" {...common}>\n          <path d=\"M5.75 3.5v9M10.25 3.5v9\" />\n        </svg>\n      );\n    case \"play\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className={glyph} aria-hidden=\"true\" {...common}>\n          <path d=\"M5 3.4v9.2l7.4-4.6z\" />\n        </svg>\n      );\n    case \"retry\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className={glyph} aria-hidden=\"true\" {...common}>\n          <path d=\"M12.9 8a4.9 4.9 0 1 1-1.45-3.48M12.9 2.6v2.6h-2.6\" />\n        </svg>\n      );\n    case \"close\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className=\"size-3.5 shrink-0\" aria-hidden=\"true\" {...common}>\n          <path d=\"M4.25 4.25l7.5 7.5M11.75 4.25l-7.5 7.5\" />\n        </svg>\n      );\n    case \"check\":\n      return (\n        <svg viewBox=\"0 0 16 16\" className={glyph} aria-hidden=\"true\" {...common}>\n          <motion.path\n            d=\"M3.5 8.5l3 3 6-7\"\n            initial={{ pathLength: instant ? 1 : 0 }}\n            animate={{ pathLength: 1 }}\n            transition={{ duration: 0.32, delay: instant ? 0 : 0.1, ease: EASE }}\n          />\n        </svg>\n      );\n  }\n}\n\nconst DIGITS = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"];\n\n/** A single odometer wheel; the CSS transition rolls it from its previous digit. */\nfunction DigitWheel({ digit, instant }: { digit: number; instant: boolean }) {\n  return (\n    <span className=\"relative inline-block h-[1.2em] overflow-hidden leading-[1.2em]\">\n      <span className=\"invisible\">0</span>\n      <span\n        className=\"absolute inset-x-0 top-0 flex flex-col items-center transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]\"\n        style={{ transform: `translateY(${-digit * 1.2}em)`, transitionDuration: instant ? \"0ms\" : undefined }}\n      >\n        {DIGITS.map((d) => (\n          <span key={d} className=\"h-[1.2em]\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\n/** Percentage whose digits roll. Wheels are keyed from the right so 9 → 10 keeps the ones wheel. */\nfunction RollingPercent({ value, instant }: { value: number; instant: boolean }) {\n  const chars = String(value).split(\"\");\n  return (\n    <span className=\"inline-flex items-baseline font-mono text-[12px] tabular-nums tracking-tight\">\n      {chars.map((c, i) => (\n        <DigitWheel key={`d${chars.length - i}`} digit={Number(c)} instant={instant} />\n      ))}\n      <span className=\"ml-px opacity-70\">%</span>\n    </span>\n  );\n}\n\n/* --------------------------------- component -------------------------------- */\n\n/**\n * A download / upload button whose fill sweeps across as work progresses. Press to start, press again\n * to pause or resume, cancel with the side button (or Escape), retry after an error. Drive it with\n * `progress` + `status`, or hand it an async `onStart(report, signal)` and it manages everything.\n */\nexport const ProgressButton = React.forwardRef<HTMLButtonElement, ProgressButtonProps>(function ProgressButton(\n  {\n    label = \"Download\",\n    runningLabel = \"Downloading\",\n    pausedLabel = \"Paused\",\n    completeLabel = \"Downloaded\",\n    errorLabel = \"Failed · Retry\",\n    progress: progressProp,\n    status: statusProp,\n    onStart,\n    onStatusChange,\n    onPause,\n    onResume,\n    onCancel,\n    onComplete,\n    onError,\n    pausable = true,\n    cancellable = true,\n    resetDelay = 2400,\n    variant = \"fill\",\n    accent = \"#FF7448\",\n    inkColor = \"#0B0D0F\",\n    errorColor = \"#F87171\",\n    spring = { stiffness: 140, damping: 24, mass: 0.6 },\n    width = \"14rem\",\n    icon,\n    className = \"\",\n    style,\n    onClick,\n    onKeyDown,\n    ...props\n  },\n  ref,\n) {\n  const reduceMotion = usePrefersReducedMotion();\n  const controlled = statusProp !== undefined;\n\n  const [internalStatus, setInternalStatus] = React.useState<ProgressButtonStatus>(\"idle\");\n  const [internalProgress, setInternalProgress] = React.useState(0);\n  const [cancelled, setCancelled] = React.useState(false);\n\n  const status = statusProp ?? internalStatus;\n  const progress = clamp01(progressProp ?? (controlled ? (status === \"complete\" ? 1 : 0) : internalProgress));\n  const pct = Math.round(progress * 100);\n\n  const mainRef = React.useRef<HTMLButtonElement>(null);\n  React.useImperativeHandle(ref, () => mainRef.current as HTMLButtonElement, []);\n\n  const controllerRef = React.useRef<AbortController | null>(null);\n  const gateRef = React.useRef<Gate | null>(null);\n  const resetTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const progressId = React.useId();\n  const descriptionId = React.useId();\n\n  // Abort the running task and clear timers on unmount.\n  React.useEffect(\n    () => () => {\n      clearTimeout(resetTimer.current);\n      controllerRef.current?.abort();\n      gateRef.current?.reject(new DOMException(\"Unmounted\", \"AbortError\"));\n    },\n    [],\n  );\n\n  const setStatus = (next: ProgressButtonStatus) => {\n    if (!controlled) setInternalStatus(next);\n    onStatusChange?.(next);\n  };\n\n  /* ------------------------------ fill animation ----------------------------- */\n\n  const fill = useMotionValue(0);\n  const { stiffness, damping, mass } = spring;\n  const target = status === \"idle\" ? 0 : status === \"complete\" ? 1 : progress;\n  React.useEffect(() => {\n    if (reduceMotion) {\n      fill.set(target);\n      return;\n    }\n    // Drain quickly on reset / cancel; otherwise ease towards the reported value with the spring.\n    const controls = animate(\n      fill,\n      target,\n      target < fill.get() ? { duration: 0.45, ease: EASE } : { type: \"spring\", stiffness, damping, mass },\n    );\n    return () => controls.stop();\n  }, [fill, target, reduceMotion, stiffness, damping, mass]);\n\n  const clipPath = useTransform(fill, (v) => `inset(0 ${((1 - v) * 100).toFixed(2)}% 0 0)`);\n  const edgeLeft = useTransform(fill, (v) => `${(v * 100).toFixed(2)}%`);\n  const edgeOpacity = useTransform(fill, (v) => (v > 0.002 && v < 0.998 ? 1 : 0));\n\n  /* --------------------------------- actions --------------------------------- */\n\n  const start = () => {\n    setCancelled(false);\n    if (controlled) {\n      setStatus(\"running\");\n      return;\n    }\n    if (!onStart) return;\n    clearTimeout(resetTimer.current);\n    controllerRef.current?.abort();\n    const controller = new AbortController();\n    controllerRef.current = controller;\n    gateRef.current = null;\n    setInternalProgress(0);\n    setStatus(\"running\");\n\n    const isCurrent = () => controllerRef.current === controller && !controller.signal.aborted;\n    const report: ProgressReport = (value) => {\n      if (!isCurrent()) {\n        const stopped = Promise.reject(controller.signal.reason ?? new DOMException(\"Aborted\", \"AbortError\"));\n        stopped.catch(noop);\n        return stopped;\n      }\n      setInternalProgress(clamp01(value));\n      return gateRef.current?.promise ?? Promise.resolve();\n    };\n\n    Promise.resolve()\n      .then(() => onStart(report, controller.signal))\n      .then(\n        () => {\n          if (!isCurrent()) return;\n          gateRef.current = null;\n          setInternalProgress(1);\n          setStatus(\"complete\");\n          onComplete?.();\n          if (resetDelay > 0) {\n            resetTimer.current = setTimeout(() => {\n              if (controllerRef.current !== controller) return;\n              controllerRef.current = null;\n              setInternalProgress(0);\n              setStatus(\"idle\");\n            }, resetDelay);\n          }\n        },\n        (error: unknown) => {\n          if (!isCurrent()) return;\n          gateRef.current = null;\n          setStatus(\"error\");\n          onError?.(error);\n        },\n      );\n  };\n\n  const pause = () => {\n    if (!controlled) gateRef.current ??= createGate();\n    setStatus(\"paused\");\n    onPause?.();\n  };\n\n  const resume = () => {\n    gateRef.current?.resolve();\n    gateRef.current = null;\n    setStatus(\"running\");\n    onResume?.();\n  };\n\n  const cancel = () => {\n    if (status !== \"running\" && status !== \"paused\") return;\n    if (!controlled) {\n      const controller = controllerRef.current;\n      controllerRef.current = null;\n      controller?.abort();\n      gateRef.current?.reject(controller?.signal.reason ?? new DOMException(\"Cancelled\", \"AbortError\"));\n      gateRef.current = null;\n      setInternalProgress(0);\n    }\n    setCancelled(true);\n    setStatus(\"idle\");\n    onCancel?.();\n    // The cancel button is about to disappear; keep focus on the main control.\n    mainRef.current?.focus();\n  };\n\n  const busy = status === \"running\" || status === \"paused\";\n  const showCancel = cancellable && busy;\n  const inert = status === \"complete\" || (status === \"running\" && !pausable);\n\n  /* ------------------------------ accessible text ----------------------------- */\n\n  const accessibleName =\n    status === \"running\"\n      ? pausable\n        ? `Pause ${label}`\n        : runningLabel\n      : status === \"paused\"\n        ? `Resume ${label}`\n        : status === \"complete\"\n          ? completeLabel\n          : status === \"error\"\n            ? `Retry ${label}`\n            : label;\n\n  const description =\n    status === \"running\"\n      ? `${runningLabel}, ${pct}%`\n      : status === \"paused\"\n        ? `${pausedLabel} at ${pct}%`\n        : status === \"error\"\n          ? `${errorLabel}, stopped at ${pct}%`\n          : undefined;\n\n  // Milestones only: the live region changes at 25 / 50 / 75 / 100 %, not on every report.\n  const milestone = pct >= 100 ? 100 : Math.floor(pct / 25) * 25;\n  const announcement =\n    status === \"running\"\n      ? milestone > 0\n        ? `${milestone}%`\n        : `${runningLabel}…`\n      : status === \"paused\"\n        ? `${pausedLabel} at ${pct}%`\n        : status === \"complete\"\n          ? completeLabel\n          : status === \"error\"\n            ? errorLabel\n            : cancelled\n              ? `${label} cancelled`\n              : \"\";\n\n  /* ---------------------------------- visuals --------------------------------- */\n\n  const iconFor = (s: ProgressButtonStatus): React.ReactNode => {\n    if (s === \"running\") return pausable ? <Glyph kind=\"pause\" /> : <Glyph kind=\"download\" />;\n    if (s === \"paused\") return <Glyph kind=\"play\" />;\n    if (s === \"complete\") return <Glyph kind=\"check\" instant={reduceMotion} />;\n    if (s === \"error\") return <Glyph kind=\"retry\" />;\n    return icon ? <span className=\"inline-flex shrink-0 [&_svg]:size-4\">{icon}</span> : <Glyph kind=\"download\" />;\n  };\n\n  const textFor = (s: ProgressButtonStatus) =>\n    s === \"running\" ? runningLabel : s === \"paused\" ? pausedLabel : s === \"complete\" ? completeLabel : s === \"error\" ? errorLabel : label;\n\n  // One face per status, stacked in a single grid cell so the old and new face crossfade in place.\n  const face = (\n    <span className=\"grid w-full grid-cols-1 grid-rows-1 *:col-start-1 *:row-start-1\">\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={status}\n          className={`flex w-full items-center gap-2.5 ${busy ? \"justify-between\" : \"justify-center\"}`}\n          initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 8, filter: \"blur(2px)\" }}\n          animate={reduceMotion ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8, filter: \"blur(2px)\" }}\n          transition={{ duration: reduceMotion ? 0.12 : 0.26, ease: EASE }}\n        >\n          <span className=\"inline-flex min-w-0 items-center gap-2\">\n            {iconFor(status)}\n            <span className=\"truncate\">{textFor(status)}</span>\n          </span>\n          {busy && <RollingPercent value={pct} instant={reduceMotion} />}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n\n  const fillColor = status === \"error\" ? errorColor : accent;\n  const padding = \"px-4\";\n\n  return (\n    <span\n      role=\"group\"\n      aria-label={label}\n      className=\"relative inline-flex max-w-full items-center\"\n      style={{ [\"--pb-accent\" as string]: accent, [\"--pb-fill\" as string]: fillColor, [\"--pb-ink\" as string]: inkColor }}\n    >\n      <motion.span\n        className=\"relative inline-flex min-w-0\"\n        animate={status === \"error\" && !reduceMotion ? { x: [0, -4, 4, -2, 2, 0] } : { x: 0 }}\n        transition={{ duration: 0.4, ease: \"easeOut\" }}\n      >\n        <button\n          ref={mainRef}\n          type=\"button\"\n          aria-label={accessibleName}\n          aria-describedby={description ? descriptionId : undefined}\n          aria-disabled={inert || undefined}\n          aria-busy={status === \"running\" || undefined}\n          data-status={status}\n          onClick={(event) => {\n            onClick?.(event);\n            if (event.defaultPrevented || inert) return;\n            if (status === \"idle\" || status === \"error\") start();\n            else if (status === \"running\") pause();\n            else if (status === \"paused\") resume();\n          }}\n          onKeyDown={(event) => {\n            onKeyDown?.(event);\n            if (!event.defaultPrevented && event.key === \"Escape\" && cancellable && busy) {\n              event.preventDefault();\n              cancel();\n            }\n          }}\n          className={`group/pb relative isolate inline-flex h-11 min-w-0 max-w-full select-none items-center overflow-hidden rounded-full border border-zinc-900/12 bg-white text-sm font-medium tracking-tight text-zinc-900 shadow-[0_1px_0_rgba(0,0,0,0.04)] outline-hidden transition-[border-color,background-color,box-shadow] duration-200 hover:border-zinc-900/25 focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-solid focus-visible:outline-[var(--pb-accent)] disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-default dark:border-white/12 dark:bg-white/[0.04] dark:text-zinc-100 dark:shadow-none dark:hover:border-white/25 ${padding} ${className}`}\n          style={{ width, ...style }}\n          {...props}\n        >\n          {/* Base face (neutral ink). */}\n          <span aria-hidden=\"true\" className=\"relative flex w-full\">\n            {face}\n          </span>\n\n          {variant === \"fill\" ? (\n            <>\n              {/* Filled copy of the same face, revealed left → right by clip-path. */}\n              <motion.span\n                aria-hidden=\"true\"\n                className={`absolute inset-0 flex items-center bg-[var(--pb-fill)] text-[var(--pb-ink)] transition-[background-color] duration-300 ${padding}`}\n                style={{ clipPath }}\n              >\n                {face}\n                <span\n                  className={`pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(-45deg,transparent_0_5px,rgba(0,0,0,0.09)_5px_10px)] transition-opacity duration-300 ${\n                    status === \"paused\" ? \"opacity-100\" : \"opacity-0\"\n                  }`}\n                />\n              </motion.span>\n              {/* Leading edge of the sweep. */}\n              <motion.span\n                aria-hidden=\"true\"\n                className=\"pointer-events-none absolute inset-y-0 w-px -translate-x-px bg-white/70 mix-blend-overlay\"\n                style={{ left: edgeLeft, opacity: edgeOpacity }}\n              />\n            </>\n          ) : (\n            <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-x-4 bottom-[7px] h-[2px] overflow-hidden rounded-full bg-zinc-900/[0.07] dark:bg-white/10\">\n              <motion.span className=\"absolute inset-0 origin-left rounded-full bg-[var(--pb-fill)]\" style={{ scaleX: fill }} />\n            </span>\n          )}\n        </button>\n      </motion.span>\n\n      <AnimatePresence initial={false}>\n        {showCancel && (\n          <motion.span\n            key=\"cancel\"\n            className=\"inline-flex shrink-0 overflow-hidden\"\n            initial={reduceMotion ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            animate={reduceMotion ? { opacity: 1 } : { width: 50, opacity: 1 }}\n            exit={reduceMotion ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            transition={{ duration: reduceMotion ? 0.12 : 0.3, ease: EASE }}\n          >\n            <button\n              type=\"button\"\n              aria-label={`Cancel ${label}`}\n              onClick={cancel}\n              className=\"ml-1.5 inline-flex size-11 shrink-0 items-center justify-center rounded-full border border-zinc-900/12 bg-white text-zinc-600 outline-hidden transition-colors hover:border-zinc-900/25 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-solid focus-visible:outline-[var(--pb-accent)] dark:border-white/12 dark:bg-white/[0.04] dark:text-zinc-400 dark:hover:border-white/25 dark:hover:text-zinc-100\"\n            >\n              <Glyph kind=\"close\" />\n            </button>\n          </motion.span>\n        )}\n      </AnimatePresence>\n\n      {/* Machine-readable progress for assistive tech, plus the description the button points at. */}\n      {status !== \"idle\" && (\n        <span\n          id={progressId}\n          role=\"progressbar\"\n          aria-label={`${label} progress`}\n          aria-valuemin={0}\n          aria-valuemax={100}\n          aria-valuenow={pct}\n          aria-valuetext={description ?? `${pct}%`}\n          className=\"sr-only\"\n        />\n      )}\n      <span id={descriptionId} className=\"sr-only\">\n        {description}\n      </span>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </span>\n  );\n});\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "buttons"
  ],
  "docs": "Documentation: https://wancraft.my/components/progress-button"
}
