{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "upload-cloud",
  "type": "registry:component",
  "title": "Upload Cloud",
  "description": "Empty-state art for uploads: a soft, breathing goo cloud reaches down, scoops up the file waiting below, fills a ring with a live percentage and lands on a tick.",
  "categories": [
    "illustration",
    "Empty States"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/upload-cloud.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon } from \"lucide-react\";\nimport { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type UploadCloudHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<UploadCloudHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-400)] [--hue:var(--color-sky-500)] [--hue-lo:var(--color-sky-600)]\",\n  emerald: \"[--hue-hi:var(--color-emerald-400)] [--hue:var(--color-emerald-500)] [--hue-lo:var(--color-emerald-600)]\",\n  amber: \"[--hue-hi:var(--color-amber-400)] [--hue:var(--color-amber-500)] [--hue-lo:var(--color-amber-600)]\",\n  violet: \"[--hue-hi:var(--color-violet-400)] [--hue:var(--color-violet-500)] [--hue-lo:var(--color-violet-600)]\",\n  rose: \"[--hue-hi:var(--color-rose-400)] [--hue:var(--color-rose-500)] [--hue-lo:var(--color-rose-600)]\",\n  orange: \"[--hue-hi:var(--color-orange-400)] [--hue:var(--color-orange-500)] [--hue-lo:var(--color-orange-600)]\",\n  indigo: \"[--hue-hi:var(--color-indigo-400)] [--hue:var(--color-indigo-500)] [--hue-lo:var(--color-indigo-600)]\",\n  teal: \"[--hue-hi:var(--color-teal-400)] [--hue:var(--color-teal-500)] [--hue-lo:var(--color-teal-600)]\",\n};\n\ntype Phase = \"idle\" | \"dropping\" | \"uploading\" | \"done\";\n\nexport interface UploadCloudProps {\n  title?: string;\n  description?: string;\n  fileName?: string;\n  fileSize?: string;\n  fileType?: string;\n  hue?: UploadCloudHue;\n  className?: string;\n}\n\nconst W = 400;\nconst H = 240;\nconst CX = 200;\nconst CY = 96;\nconst FILE_Y = 162;\nconst TENDRIL_REST = CY + 22;\nconst RING_R = 25;\nconst RING_C = 2 * Math.PI * RING_R;\n\nconst puffs = [\n  { cx: CX - 46, cy: CY + 8, r: 36, d: 3.4 },\n  { cx: CX + 2, cy: CY - 20, r: 50, d: 4.2 },\n  { cx: CX + 52, cy: CY + 6, r: 34, d: 3.8 },\n];\n\nfunction Puff({ cx, cy, r, d, live, i }: (typeof puffs)[number] & { live: boolean; i: number }) {\n  return (\n    <motion.circle\n      animate={live ? { cy: [cy, cy - 2.5, cy + 1.5, cy], r: [r, r + 2, r - 1, r] } : { cy, r }}\n      cx={cx}\n      cy={cy}\n      initial={false}\n      r={r}\n      transition={live ? { delay: i * 0.6, duration: d, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.3 }}\n    />\n  );\n}\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport function UploadCloud({\n  title = \"Drop files to upload\",\n  description = \"PDF, PNG or CSV, up to 25 MB each.\",\n  fileName = \"q3-brief.pdf\",\n  fileSize = \"2.4 MB\",\n  fileType = \"PDF\",\n  hue = \"sky\",\n  className,\n}: UploadCloudProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const live = inView && !reduce;\n  const [phase, setPhase] = useState<Phase>(\"idle\");\n  const fileY = useMotionValue(0);\n  const fileOpacity = useMotionValue(1);\n  const tendril = useMotionValue(TENDRIL_REST);\n  const progress = useMotionValue(0);\n  const gulp = useMotionValue(1);\n  const dash = useTransform(progress, (p) => RING_C * (1 - p));\n  const percent = useTransform(progress, (p) => `${Math.round(p * 100)}%`);\n\n  useEffect(() => {\n    if (reduce) {\n      progress.set(1);\n      fileOpacity.set(0);\n      setPhase(\"done\");\n      return;\n    }\n    if (!inView) return;\n    let alive = true;\n    const timers: ReturnType<typeof setTimeout>[] = [];\n    const stops: { stop: () => void }[] = [];\n    const wait = (ms: number) => new Promise<void>((r) => timers.push(setTimeout(r, ms)));\n    const run = <T extends { stop: () => void }>(c: T) => {\n      stops.push(c);\n      return c;\n    };\n\n    (async () => {\n      while (alive) {\n        setPhase(\"idle\");\n        fileY.set(8);\n        tendril.set(TENDRIL_REST);\n        progress.set(0);\n        run(animate(fileY, 0, { duration: 0.4, ease: [0.23, 1, 0.32, 1] }));\n        run(animate(fileOpacity, 1, { duration: 0.3 }));\n        await wait(1600);\n        if (!alive) return;\n        setPhase(\"dropping\");\n        await run(animate(tendril, FILE_Y + 6, { duration: 0.42, ease: [0.23, 1, 0.32, 1] }));\n        if (!alive) return;\n        run(animate(tendril, TENDRIL_REST, { duration: 0.55, ease: [0.65, 0, 0.35, 1] }));\n        await run(animate(fileY, -94, { duration: 0.55, ease: [0.65, 0, 0.35, 1] }));\n        if (!alive) return;\n        fileOpacity.set(0);\n        run(animate(gulp, [1, 1.045, 0.99, 1], { duration: 0.5, ease: [0.23, 1, 0.32, 1] }));\n        setPhase(\"uploading\");\n        await run(animate(progress, [0, 0.42, 0.48, 1], { duration: 2.1, ease: \"easeInOut\", times: [0, 0.35, 0.55, 1] }));\n        if (!alive) return;\n        setPhase(\"done\");\n        run(animate(gulp, [1, 1.035, 1], { duration: 0.45, ease: [0.23, 1, 0.32, 1] }));\n        await wait(2400);\n      }\n    })();\n\n    return () => {\n      alive = false;\n      for (const t of timers) clearTimeout(t);\n      for (const s of stops) s.stop();\n    };\n  }, [reduce, inView, fileY, fileOpacity, tendril, progress, gulp]);\n\n  const busy = phase === \"uploading\";\n  const done = phase === \"done\";\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[440px] overflow-hidden rounded-2xl border bg-card p-3 shadow-xs/5 sm:p-4\",\n        hues[hue],\n        className,\n      )}\n    >\n      <div className=\"relative overflow-hidden rounded-xl border border-foreground/15 border-dashed px-3 pt-3 pb-4 sm:px-5 sm:pb-5\">\n        <div\n          aria-hidden\n          className=\"dot-grid pointer-events-none absolute inset-0 opacity-50 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n        />\n        <div className=\"relative mx-auto aspect-[40/24] w-full max-w-[400px]\">\n          <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\" viewBox={`0 0 ${W} ${H}`}>\n            <defs>\n              <linearGradient gradientUnits=\"userSpaceOnUse\" id={`${uid}-cloud`} x1={CX - 60} x2={CX + 70} y1={CY - 70} y2={CY + 60}>\n                <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n                <stop offset=\"1\" stopColor=\"var(--hue-lo)\" />\n              </linearGradient>\n              <filter filterUnits=\"userSpaceOnUse\" height={H} id={`${uid}-goo`} width={W} x={0} y={0}>\n                <feGaussianBlur in=\"SourceGraphic\" result=\"blur\" stdDeviation=\"7\" />\n                <feColorMatrix in=\"blur\" values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 22 -9\" />\n              </filter>\n            </defs>\n\n            <motion.ellipse\n              animate={{ opacity: phase === \"idle\" ? 1 : 0 }}\n              className=\"fill-foreground/8\"\n              cx={CX}\n              cy={FILE_Y + 66}\n              initial={false}\n              rx={24}\n              ry={3.5}\n              transition={{ duration: 0.25 }}\n            />\n            <motion.g\n              animate={live && phase === \"idle\" ? { y: [0, -3, 0] } : { y: 0 }}\n              transition={live && phase === \"idle\" ? { duration: 2.4, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } : { duration: 0.2 }}\n            >\n              <motion.g style={{ opacity: fileOpacity, y: fileY }}>\n                <g transform={`translate(${CX - 22} ${FILE_Y})`}>\n                  <path className=\"fill-white stroke-black/10\" d=\"M6 0 H30 L44 14 V52 a6 6 0 0 1 -6 6 H6 a6 6 0 0 1 -6 -6 V6 a6 6 0 0 1 6 -6 Z\" />\n                  <path className=\"fill-black/10\" d=\"M30 0 V10 a4 4 0 0 0 4 4 H44 Z\" />\n                  <rect className=\"fill-rose-500\" height={11} rx={3} width={24} x={-4} y={30} />\n                  <text className=\"fill-white font-semibold\" fontSize={7} textAnchor=\"middle\" x={8} y={38}>\n                    {fileType}\n                  </text>\n                  <rect className=\"fill-black/10\" height={3} rx={1.5} width={18} x={24} y={34} />\n                  <rect className=\"fill-black/10\" height={3} rx={1.5} width={30} x={8} y={45} />\n                </g>\n              </motion.g>\n            </motion.g>\n\n            <motion.g style={{ originX: `${CX}px`, originY: `${CY + 50}px`, scale: gulp, transformBox: \"view-box\" }}>\n              <g fill={`url(#${uid}-cloud)`} filter={`url(#${uid}-goo)`}>\n                {puffs.map((p, i) => (\n                  <Puff i={i} key={p.cx} live={live} {...p} />\n                ))}\n                <rect height={44} rx={22} width={176} x={CX - 88} y={CY + 6} />\n                <motion.circle cx={CX} cy={tendril} r={17} />\n              </g>\n              <circle className=\"fill-white/15\" cx={CX - 12} cy={CY - 46} r={18} />\n            </motion.g>\n\n            <motion.g\n              animate={phase === \"idle\" || phase === \"dropping\" ? { opacity: 1, y: live ? [0, -4, 0] : 0 } : { opacity: 0, y: -10 }}\n              className=\"stroke-white\"\n              fill=\"none\"\n              initial={false}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={5}\n              transition={\n                (phase === \"idle\" || phase === \"dropping\") && live\n                  ? { opacity: { duration: 0.25 }, y: { duration: 1.6, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY } }\n                  : { duration: 0.2, ease: \"easeOut\" }\n              }\n            >\n              <path d={`M${CX} ${CY + 12} V${CY - 18}`} />\n              <path d={`M${CX - 13} ${CY - 6} L${CX} ${CY - 19} L${CX + 13} ${CY - 6}`} />\n            </motion.g>\n\n            <motion.g\n              animate={{ opacity: busy || done ? 1 : 0, scale: busy || done ? 1 : 0.9 }}\n              initial={false}\n              style={{ originX: `${CX}px`, originY: `${CY - 3}px`, transformBox: \"view-box\" }}\n              transition={{ duration: 0.25, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <circle className=\"fill-none stroke-white/25\" cx={CX} cy={CY - 3} r={RING_R} strokeWidth={5} />\n              <motion.circle\n                className=\"fill-none stroke-white\"\n                cx={CX}\n                cy={CY - 3}\n                r={RING_R}\n                strokeDasharray={RING_C}\n                strokeLinecap=\"round\"\n                strokeWidth={5}\n                style={{ strokeDashoffset: dash }}\n                transform={`rotate(-90 ${CX} ${CY - 3})`}\n              />\n              <motion.circle\n                animate={{ scale: done ? 1 : 0.6, opacity: done ? 1 : 0 }}\n                className=\"fill-white\"\n                cx={CX}\n                cy={CY - 3}\n                initial={false}\n                r={RING_R + 2.5}\n                style={{ originX: \"50%\", originY: \"50%\" }}\n                transition={done ? { type: \"spring\", stiffness: 420, damping: 22 } : { duration: 0.2 }}\n              />\n              <motion.path\n                animate={{ pathLength: done ? 1 : 0, opacity: done ? 1 : 0 }}\n                className=\"fill-none stroke-(--hue)\"\n                d={`M${CX - 11} ${CY - 2} l7.5 7.5 l14 -15`}\n                initial={false}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth={5}\n                transition={done ? { delay: 0.12, duration: 0.3, ease: [0.23, 1, 0.32, 1] } : { duration: 0.15 }}\n              />\n            </motion.g>\n          </svg>\n\n          <motion.span\n            animate={{ opacity: busy ? 1 : 0 }}\n            aria-hidden\n            className=\"absolute -translate-x-1/2 -translate-y-1/2 font-semibold text-[11px] text-white tabular-nums\"\n            initial={false}\n            style={{ left: `${(CX / W) * 100}%`, top: `${((CY - 3) / H) * 100}%` }}\n            transition={{ duration: 0.2 }}\n          >\n            {percent}\n          </motion.span>\n          <div\n            aria-live=\"polite\"\n            className=\"absolute inset-x-0 flex -translate-y-1/2 justify-center text-xs\"\n            style={{ top: `${((FILE_Y + 29) / H) * 100}%` }}\n          >\n            <motion.div\n              animate={{ opacity: busy || done ? 1 : 0, y: busy || done ? 0 : 4 }}\n              className=\"flex items-center gap-2 rounded-full border bg-popover py-0.5 pr-2.5 pl-1 shadow-xs/5\"\n              initial={false}\n              transition={{ duration: 0.22, ease: [0.23, 1, 0.32, 1] }}\n            >\n              <span className=\"rounded-full bg-rose-500/12 px-1.5 py-px font-semibold text-[10px] text-rose-600 dark:text-rose-400\">\n                {fileType}\n              </span>\n              <span className=\"font-medium text-foreground\">{fileName}</span>\n              <span className=\"relative inline-flex w-[4.5rem] justify-end text-muted-foreground tabular-nums\">\n                <motion.span animate={{ opacity: done ? 0 : 1 }} initial={false} transition={{ duration: 0.15 }}>\n                  {fileSize}\n                </motion.span>\n                <motion.span\n                  animate={{ opacity: done ? 1 : 0, filter: done ? \"blur(0px)\" : \"blur(2px)\" }}\n                  className=\"absolute inset-y-0 right-0 flex items-center gap-1 font-medium text-success-foreground\"\n                  initial={false}\n                  transition={{ duration: 0.2 }}\n                >\n                  <CheckIcon className=\"size-3\" strokeWidth={2.5} />\n                  Uploaded\n                </motion.span>\n              </span>\n            </motion.div>\n          </div>\n        </div>\n\n        <div className=\"relative mt-3 text-center\">\n          <p className=\"text-balance font-medium text-foreground text-sm\">{title}</p>\n          <p className=\"mx-auto mt-1 max-w-[30ch] text-pretty text-muted-foreground text-xs\">{description}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}