{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "download-button",
  "title": "Download Button",
  "description": "Button that morphs from Download to a live progress fill and a drawn check mark.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/download-button/download-button.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\ntype State = \"idle\" | \"loading\" | \"done\"\n\nexport interface DownloadButtonProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\" | \"children\"> {\n  /** File to download. Triggered once the animation completes. */\n  href?: string\n  /** Suggested file name for `href`. */\n  fileName?: string\n  /** Run your own download. The progress bar waits for the promise. */\n  onDownload?: () => Promise<unknown> | void\n  /** Controlled progress, 0 to 100. When set, the bar follows it instead of simulating. */\n  progress?: number\n  /** Length of the simulated progress, in seconds. Default: 2 */\n  duration?: number\n  /** Return to idle this many ms after finishing (0 = stay done). Default: 2200 */\n  resetAfter?: number\n  /** Idle label. Default: \"Download\" */\n  label?: string\n  /** Finished label. Default: \"Downloaded\" */\n  doneLabel?: string\n  /** Show the percentage while loading. Default: true */\n  showPercent?: boolean\n  /** Progress fill color (any CSS color). Default: 25% of the background color */\n  fillColor?: string\n}\n\nconst Arrow = () => (\n  <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M8 2.5v8M4.5 7 8 10.5 11.5 7M3 13.5h10\" />\n  </svg>\n)\n\nconst Check = () => (\n  <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <motion.path\n      d=\"M3 8.5 6.5 12 13 4.5\"\n      initial={{ pathLength: 0 }}\n      animate={{ pathLength: 1 }}\n      transition={{ duration: 0.35, ease: \"easeOut\", delay: 0.05 }}\n    />\n  </svg>\n)\n\nexport function DownloadButton({\n  href,\n  fileName,\n  onDownload,\n  progress,\n  duration = 2,\n  resetAfter = 2200,\n  label = \"Download\",\n  doneLabel = \"Downloaded\",\n  showPercent = true,\n  fillColor = \"color-mix(in oklab, var(--background) 25%, transparent)\",\n  className,\n  disabled,\n  ...props\n}: DownloadButtonProps) {\n  const reduced = useReducedMotion()\n  const [state, setState] = useState<State>(\"idle\")\n  const value = useMotionValue(0)\n  const width = useTransform(value, (v) => `${v}%`)\n  const percent = useTransform(value, (v) => `${Math.round(v)}%`)\n  const resetTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n\n  const finish = () => {\n    setState(\"done\")\n    if (href) {\n      const a = document.createElement(\"a\")\n      a.href = href\n      a.download = fileName ?? \"\"\n      a.click()\n    }\n    if (resetAfter > 0) {\n      resetTimer.current = setTimeout(() => {\n        setState(\"idle\")\n        value.set(0)\n      }, resetAfter)\n    }\n  }\n\n  // Controlled mode: follow the `progress` prop\n  useEffect(() => {\n    if (progress === undefined || state !== \"loading\") return\n    const controls = animate(value, Math.min(Math.max(progress, 0), 100), { duration: 0.3 })\n    if (progress >= 100) controls.then(() => finish())\n    return () => controls.stop()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [progress, state])\n\n  useEffect(() => () => clearTimeout(resetTimer.current), [])\n\n  const start = async () => {\n    if (state !== \"idle\") return\n    setState(\"loading\")\n    value.set(0)\n    if (progress !== undefined) return // parent drives progress\n\n    const task = Promise.resolve(onDownload?.())\n    // Simulated bar eases toward 90% until the task settles, then completes\n    const sim = animate(value, 90, { duration: reduced ? 0 : duration, ease: [0.3, 0.8, 0.4, 1] })\n    await Promise.all([task, sim.then(() => undefined)]).catch(() => undefined)\n    await animate(value, 100, { duration: reduced ? 0 : 0.25 })\n    finish()\n  }\n\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      disabled={disabled}\n      onClick={start}\n      aria-live=\"polite\"\n      className={cn(\n        \"relative inline-flex h-11 min-w-44 items-center justify-center overflow-hidden rounded-full bg-foreground px-5 text-sm font-medium text-background transition-[transform,opacity] active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n    >\n      {/* progress fill */}\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"absolute inset-y-0 left-0\"\n        style={{ width, opacity: state === \"idle\" ? 0 : 1, background: fillColor }}\n      />\n      <span className=\"relative flex items-center gap-2\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={state}\n            className=\"flex items-center gap-2\"\n            initial={{ opacity: 0, y: 12, filter: \"blur(4px)\" }}\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={{ opacity: 0, y: -12, filter: \"blur(4px)\" }}\n            transition={{ type: \"spring\", stiffness: 500, damping: 34 }}\n          >\n            {state === \"idle\" && (\n              <>\n                <Arrow />\n                {label}\n              </>\n            )}\n            {state === \"loading\" && (\n              <>\n                <motion.span\n                  className=\"size-4 rounded-full border-2 border-current border-t-transparent\"\n                  animate={{ rotate: 360 }}\n                  transition={{ duration: 0.8, repeat: Infinity, ease: \"linear\" }}\n                />\n                {showPercent ? <motion.span className=\"tabular-nums\">{percent}</motion.span> : \"Downloading\"}\n              </>\n            )}\n            {state === \"done\" && (\n              <>\n                <Check />\n                {doneLabel}\n              </>\n            )}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </button>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/download-button",
  "type": "registry:component"
}