{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-download",
  "title": "Animated download",
  "description": "Animated download is a production-ready buttons component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/buttons/animated-download.tsx",
      "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DownloadProps {\n  className?: string;\n  width?: string | number;\n  height?: string | number;\n  isAnimating?: boolean;\n  onAnimationComplete?: () => void;\n}\n\nconst alphabets = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\".split(\"\");\nconst getRandomInt = (max: number) => Math.floor(Math.random() * max);\n\nexport function AnimatedDownload({\n  className,\n  isAnimating = false,\n  onAnimationComplete,\n}: DownloadProps) {\n  const [animatedProgress, setAnimatedProgress] = useState(0);\n  const [filesCount, setFilesCount] = useState(0);\n  const [timeRemainingSeconds, setTimeRemainingSeconds] = useState(154);\n  const shouldReduceMotion = useReducedMotion();\n\n  // HyperText animation state\n  const [displayText, setDisplayText] = useState(\"READY\".split(\"\"));\n  const [isTextAnimating, setIsTextAnimating] = useState(false);\n  const [targetText, setTargetText] = useState(\"READY\");\n  const [textIterations, setTextIterations] = useState(0);\n\n  // Animation configuration following Emil's principles - using proper Framer Motion easing\n  const easing = shouldReduceMotion ? \"linear\" : \"easeOut\";\n  const duration = shouldReduceMotion ? 0.3 : 2.5;\n\n  // HyperText animation logic\n  useEffect(() => {\n    const newTargetText = isAnimating ? \"DOWNLOADING\" : \"READY\";\n    if (newTargetText !== targetText) {\n      setTargetText(newTargetText);\n      setTextIterations(0);\n      setIsTextAnimating(true);\n    }\n  }, [isAnimating, targetText]);\n\n  useEffect(() => {\n    if (!isTextAnimating) return;\n\n    const interval = setInterval(() => {\n      if (textIterations < targetText.length) {\n        setDisplayText(() =>\n          targetText.split(\"\").map((l, i) =>\n            l === \" \"\n              ? l\n              : i <= textIterations\n                ? targetText[i]\n                : alphabets[getRandomInt(26)],\n          ),\n        );\n        setTextIterations((prev) => prev + 0.1);\n      } else {\n        setIsTextAnimating(false);\n        setDisplayText(targetText.split(\"\"));\n        clearInterval(interval);\n      }\n    }, 800 / (targetText.length * 10));\n\n    return () => clearInterval(interval);\n  }, [isTextAnimating, targetText, textIterations]);\n\n  // Fix React setState error by using useEffect to call onAnimationComplete\n  useEffect(() => {\n    if (animatedProgress >= 100 && isAnimating) {\n      const timer = setTimeout(() => {\n        onAnimationComplete?.();\n      }, 100);\n      return () => clearTimeout(timer);\n    }\n  }, [animatedProgress, isAnimating, onAnimationComplete]);\n\n  useEffect(() => {\n    if (!isAnimating) {\n      setAnimatedProgress(0);\n      setFilesCount(0);\n      setTimeRemainingSeconds(154);\n      return;\n    }\n\n    // Animate progress from 0 to 100\n    const progressInterval = setInterval(() => {\n      setAnimatedProgress((prev) => {\n        const next = prev + 1;\n        // Update files count based on progress\n        setFilesCount(Math.floor((next / 100) * 1000));\n\n        // Update time remaining in seconds\n        setTimeRemainingSeconds(\n          Math.max(0, 154 - Math.floor((next / 100) * 154))\n        );\n\n        if (next >= 100) {\n          clearInterval(progressInterval);\n          return 100;\n        }\n        return next;\n      });\n    }, duration * 10);\n\n    return () => {\n      clearInterval(progressInterval);\n    };\n  }, [isAnimating, duration]);\n\n  // Format time from seconds to \"Xmin XXsec\"\n  const formatTime = (seconds: number) => {\n    const minutes = Math.floor(seconds / 60);\n    const remainingSeconds = seconds % 60;\n    return `${minutes}min ${remainingSeconds.toString().padStart(2, \"0\")}sec`;\n  };\n\n  // Motion variants following Emil's interruptible animations principle\n  const containerVariants = {\n    hidden: {\n      opacity: 0,\n      y: shouldReduceMotion ? 0 : 20,\n      transition: { duration: 0.2, ease: easing },\n    },\n    visible: {\n      opacity: 1,\n      y: 0,\n      transition: { duration: 0.3, ease: easing },\n    },\n  };\n\n  // Updated chevron animation - simple smooth up and down motion\n  const chevronVariants = {\n    idle: { y: 0, opacity: 0.7 },\n    animating: {\n      y: shouldReduceMotion ? 0 : [0, 8, 0], // Smooth up and down oscillation, lower range\n      opacity: shouldReduceMotion ? 0.7 : [0.7, .9, .7], // High at peak, fade out as it comes back up\n      transition: {\n        duration: 1.5,\n        ease: \"easeInOut\",\n        repeat: isAnimating ? Infinity : 0,\n        repeatType: \"loop\" as const,\n      },\n    },\n  };\n\n  const chevron2Variants = {\n    idle: { y: 14, opacity: 0.5 }, // Slightly below first chevron\n    animating: {\n      y: shouldReduceMotion ? 8 : [14, 18, 14], // Smooth up and down oscillation, lower range\n      opacity: shouldReduceMotion ? 0.5 : [0.5, 1, 0.5], // Start low, peak when first chevron fades, then back to medium\n      transition: {\n        duration: 1.5,\n        ease: \"easeInOut\",\n        repeat: isAnimating ? Infinity : 0,\n        repeatType: \"loop\" as const,\n        delay: 0.3, // Adjusted timing for better handoff\n      },\n    },\n  };\n\n  // Sequential dots animation - appear 1,2,3 then disappear 1,2,3\n  const dotsVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        staggerChildren: 0.2,\n        delayChildren: 0.1,\n      },\n    },\n  };\n\n  const dotVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: [0, 1, 1, 0],\n      transition: {\n        duration: 1.5,\n        repeat: Infinity,\n        repeatType: \"loop\" as const,\n        ease: \"easeInOut\",\n      },\n    },\n  };\n\n  return (\n    <motion.div\n      className={`w-full max-w-lg ${className || \"\"}`}\n      variants={containerVariants}\n      initial=\"hidden\"\n      animate=\"visible\"\n    >\n      {/* Top header row */}\n      <div className=\"flex items-center mb-2\">\n        {/* Animated ChevronDown icons - pulsing up and down */}\n        <div\n          className={cn(\n            \"flex -mt-3 flex-col items-center justify-center w-8 h-16 overflow-hidden relative\"\n          )}\n        >\n          <motion.div\n            className=\"absolute\"\n            variants={chevronVariants}\n            animate={isAnimating ? \"animating\" : \"idle\"}\n          >\n            <ChevronDown size={24} className=\"text-primary\" />\n          </motion.div>\n          <motion.div\n            className=\"absolute\"\n            variants={chevron2Variants}\n            animate={isAnimating ? \"animating\" : \"idle\"}\n          >\n            <ChevronDown size={24} className=\"text-primary\" />\n          </motion.div>\n        </div>\n\n        {/* DOWNLOADING/READY banner - using inline HyperText animation */}\n        <div className=\"relative ml-2 flex-1 max-w-xs\">\n          <svg\n            width=\"50%\"\n            height=\"32\"\n            viewBox=\"0 0 107 15\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            className=\"absolute top-1/2 left-0 transform -translate-y-1/2 w-1/2 fill-foreground\"\n            preserveAspectRatio=\"none\"\n          >\n            <path\n              d=\"M0.445312 0.5H106.103V8.017L99.2813 14.838H0.445312V0.5Z\"\n            />\n          </svg>\n          <div className=\"relative px-4 py-1.5 font-mono font-bold text-sm text-black\">\n            <div className=\"flex items-center\">\n              <div className=\"flex font-mono font-bold text-black\">\n                {displayText.map((letter, i) => (\n                  <motion.span\n                    key={`${targetText}-${i}`}\n                    className={cn(\"font-mono dark:text-black text-white font-bold\", letter === \" \" ? \"w-3\" : \"\")}\n                    initial={{ opacity: 0, y: -10 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: 3 }}\n                  >\n                    {letter}\n                  </motion.span>\n                ))}\n              </div>\n              {isAnimating && (\n                <motion.div\n                  className=\"ml-1 flex text-white dark:text-black\"\n                  variants={dotsVariants}\n                  initial=\"hidden\"\n                  animate=\"visible\"\n                >\n                  <motion.span variants={dotVariants}>.</motion.span>\n                  <motion.span variants={dotVariants}>.</motion.span>\n                  <motion.span variants={dotVariants}>.</motion.span>\n                </motion.div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Thick separator bar */}\n      <div className=\"w-full h-1 bg-foreground mb-3 rounded-full\" />\n\n      {/* Labels row */}\n      <div className=\"flex items-center mb-1\">\n        <div className=\"w-32\">\n          <div className=\"text-xs font-mono\">PROGRESS</div>\n        </div>\n\n        <div className=\"flex ml-6\">\n          <div className=\"w-28 text-left\">\n            <div className=\"text-xs font-mono\">EST. TIME</div>\n          </div>\n          <div className=\"w-28 text-left\">\n            <div className=\"text-xs font-mono\">FILES COPIED:</div>\n          </div>\n        </div>\n      </div>\n\n      {/* Values row - progress bar and info values */}\n      <div className=\"flex items-center\">\n        {/* Animated Progress bar */}\n        <div className=\"w-32\">\n          <div className=\"w-full h-2.5 border dark:border-white border-black bg-transparent rounded-full flex items-center px-0.5\">\n            <motion.div\n              className=\"h-1 dark:bg-white bg-black rounded-full\"\n              initial={{ width: \"0%\" }}\n              animate={{\n                width: `${animatedProgress}%`,\n              }}\n              transition={{\n                duration: shouldReduceMotion ? 0.1 : 0.3,\n                ease: easing,\n              }}\n            />\n          </div>\n        </div>\n\n        {/* Animated info values */}\n        <div className=\"flex ml-6\">\n          <div className=\"w-28 text-left\">\n            <div className=\"text-sm font-mono\">\n              {formatTime(timeRemainingSeconds)}\n            </div>\n          </div>\n          <div className=\"w-28 text-left\">\n            <div className=\"text-sm font-mono\">\n              {filesCount.toLocaleString()}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Static bottom bar - always visible */}\n      <div className=\"w-3/4 h-0.5 bg-primary mt-4 rounded-full\" />\n    </motion.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/buttons/animated-download.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.72)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.52)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.36)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.52)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.36)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#ec5c13",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Buttons",
    "registryUrl": "https://ui.isaiahbjork.com/animated-download.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/animated-download.json"
  },
  "type": "registry:component"
}