"use client"; import { CheckIcon } from "lucide-react"; import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react"; import { useEffect, useId, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type UploadCloudHue = "sky" | "emerald" | "amber" | "violet" | "rose" | "orange" | "indigo" | "teal"; const hues: Record = { sky: "[--hue-hi:var(--color-sky-400)] [--hue:var(--color-sky-500)] [--hue-lo:var(--color-sky-600)]", emerald: "[--hue-hi:var(--color-emerald-400)] [--hue:var(--color-emerald-500)] [--hue-lo:var(--color-emerald-600)]", amber: "[--hue-hi:var(--color-amber-400)] [--hue:var(--color-amber-500)] [--hue-lo:var(--color-amber-600)]", violet: "[--hue-hi:var(--color-violet-400)] [--hue:var(--color-violet-500)] [--hue-lo:var(--color-violet-600)]", rose: "[--hue-hi:var(--color-rose-400)] [--hue:var(--color-rose-500)] [--hue-lo:var(--color-rose-600)]", orange: "[--hue-hi:var(--color-orange-400)] [--hue:var(--color-orange-500)] [--hue-lo:var(--color-orange-600)]", indigo: "[--hue-hi:var(--color-indigo-400)] [--hue:var(--color-indigo-500)] [--hue-lo:var(--color-indigo-600)]", teal: "[--hue-hi:var(--color-teal-400)] [--hue:var(--color-teal-500)] [--hue-lo:var(--color-teal-600)]", }; type Phase = "idle" | "dropping" | "uploading" | "done"; export interface UploadCloudProps { title?: string; description?: string; fileName?: string; fileSize?: string; fileType?: string; hue?: UploadCloudHue; className?: string; } const W = 400; const H = 240; const CX = 200; const CY = 96; const FILE_Y = 162; const TENDRIL_REST = CY + 22; const RING_R = 25; const RING_C = 2 * Math.PI * RING_R; const puffs = [ { cx: CX - 46, cy: CY + 8, r: 36, d: 3.4 }, { cx: CX + 2, cy: CY - 20, r: 50, d: 4.2 }, { cx: CX + 52, cy: CY + 6, r: 34, d: 3.8 }, ]; function Puff({ cx, cy, r, d, live, i }: (typeof puffs)[number] & { live: boolean; i: number }) { return ( ); } function useCalmMotion() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export function UploadCloud({ title = "Drop files to upload", description = "PDF, PNG or CSV, up to 25 MB each.", fileName = "q3-brief.pdf", fileSize = "2.4 MB", fileType = "PDF", hue = "sky", className, }: UploadCloudProps) { const uid = useId().replace(/:/g, ""); const reduce = useCalmMotion(); const root = useRef(null); const inView = useInView(root); const live = inView && !reduce; const [phase, setPhase] = useState("idle"); const fileY = useMotionValue(0); const fileOpacity = useMotionValue(1); const tendril = useMotionValue(TENDRIL_REST); const progress = useMotionValue(0); const gulp = useMotionValue(1); const dash = useTransform(progress, (p) => RING_C * (1 - p)); const percent = useTransform(progress, (p) => `${Math.round(p * 100)}%`); useEffect(() => { if (reduce) { progress.set(1); fileOpacity.set(0); setPhase("done"); return; } if (!inView) return; let alive = true; const timers: ReturnType[] = []; const stops: { stop: () => void }[] = []; const wait = (ms: number) => new Promise((r) => timers.push(setTimeout(r, ms))); const run = void }>(c: T) => { stops.push(c); return c; }; (async () => { while (alive) { setPhase("idle"); fileY.set(8); tendril.set(TENDRIL_REST); progress.set(0); run(animate(fileY, 0, { duration: 0.4, ease: [0.23, 1, 0.32, 1] })); run(animate(fileOpacity, 1, { duration: 0.3 })); await wait(1600); if (!alive) return; setPhase("dropping"); await run(animate(tendril, FILE_Y + 6, { duration: 0.42, ease: [0.23, 1, 0.32, 1] })); if (!alive) return; run(animate(tendril, TENDRIL_REST, { duration: 0.55, ease: [0.65, 0, 0.35, 1] })); await run(animate(fileY, -94, { duration: 0.55, ease: [0.65, 0, 0.35, 1] })); if (!alive) return; fileOpacity.set(0); run(animate(gulp, [1, 1.045, 0.99, 1], { duration: 0.5, ease: [0.23, 1, 0.32, 1] })); setPhase("uploading"); await run(animate(progress, [0, 0.42, 0.48, 1], { duration: 2.1, ease: "easeInOut", times: [0, 0.35, 0.55, 1] })); if (!alive) return; setPhase("done"); run(animate(gulp, [1, 1.035, 1], { duration: 0.45, ease: [0.23, 1, 0.32, 1] })); await wait(2400); } })(); return () => { alive = false; for (const t of timers) clearTimeout(t); for (const s of stops) s.stop(); }; }, [reduce, inView, fileY, fileOpacity, tendril, progress, gulp]); const busy = phase === "uploading"; const done = phase === "done"; return (
{fileType} {puffs.map((p, i) => ( ))} {percent}
{fileType} {fileName} {fileSize} Uploaded

{title}

{description}

); }