{
  "name": "file-upload",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: File Upload\n * @version: 1.0.0\n * @date: 2025-06-26\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { UploadCloud } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport {\n  type DragEvent,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype FileStatus = \"idle\" | \"dragging\" | \"uploading\" | \"error\";\n\ninterface FileError {\n  message: string;\n  code: string;\n}\n\ninterface FileUploadProps {\n  onUploadSuccess?: (file: File) => void;\n  onUploadError?: (error: FileError) => void;\n  acceptedFileTypes?: string[];\n  maxFileSize?: number;\n  currentFile?: File | null;\n  onFileRemove?: () => void;\n  /** Duration in milliseconds for the upload simulation. Defaults to 2000ms (2s), 0 for no simulation */\n  uploadDelay?: number;\n  validateFile?: (file: File) => FileError | null;\n  className?: string;\n}\n\nconst DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB\nconst UPLOAD_STEP_SIZE = 5;\nconst FILE_SIZES = [\n  \"Bytes\",\n  \"KB\",\n  \"MB\",\n  \"GB\",\n  \"TB\",\n  \"PB\",\n  \"EB\",\n  \"ZB\",\n  \"YB\",\n] as const;\n\nconst formatBytes = (bytes: number, decimals = 2): string => {\n  if (!+bytes) return \"0 Bytes\";\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  const unit = FILE_SIZES[i] || FILE_SIZES[FILE_SIZES.length - 1];\n  return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${unit}`;\n};\n\nconst UploadIllustration = () => (\n  <div className=\"relative h-16 w-16\">\n    <svg\n      aria-label=\"Upload illustration\"\n      className=\"h-full w-full\"\n      fill=\"none\"\n      viewBox=\"0 0 100 100\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <title>Upload File Illustration</title>\n      <circle\n        className=\"stroke-gray-200 dark:stroke-gray-700\"\n        cx=\"50\"\n        cy=\"50\"\n        r=\"45\"\n        strokeDasharray=\"4 4\"\n        strokeWidth=\"2\"\n      >\n        <animateTransform\n          attributeName=\"transform\"\n          dur=\"60s\"\n          from=\"0 50 50\"\n          repeatCount=\"indefinite\"\n          to=\"360 50 50\"\n          type=\"rotate\"\n        />\n      </circle>\n\n      <path\n        className=\"fill-blue-100 stroke-blue-500 dark:fill-blue-900/30 dark:stroke-blue-400\"\n        d=\"M30 35H70C75 35 75 40 75 40V65C75 70 70 70 70 70H30C25 70 25 65 25 65V40C25 35 30 35 30 35Z\"\n        strokeWidth=\"2\"\n      >\n        <animate\n          attributeName=\"d\"\n          dur=\"2s\"\n          repeatCount=\"indefinite\"\n          values=\"\n                        M30 35H70C75 35 75 40 75 40V65C75 70 70 70 70 70H30C25 70 25 65 25 65V40C25 35 30 35 30 35Z;\n                        M30 38H70C75 38 75 43 75 43V68C75 73 70 73 70 73H30C25 73 25 68 25 68V43C25 38 30 38 30 38Z;\n                        M30 35H70C75 35 75 40 75 40V65C75 70 70 70 70 70H30C25 70 25 65 25 65V40C25 35 30 35 30 35Z\"\n        />\n      </path>\n\n      <path\n        className=\"stroke-blue-500 dark:stroke-blue-400\"\n        d=\"M30 35C30 35 35 35 40 35C45 35 45 30 50 30C55 30 55 35 60 35C65 35 70 35 70 35\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n      />\n\n      <g className=\"translate-y-2 transform\">\n        <line\n          className=\"stroke-blue-500 dark:stroke-blue-400\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"2\"\n          x1=\"50\"\n          x2=\"50\"\n          y1=\"45\"\n          y2=\"60\"\n        >\n          <animate\n            attributeName=\"y2\"\n            dur=\"2s\"\n            repeatCount=\"indefinite\"\n            values=\"60;55;60\"\n          />\n        </line>\n        <polyline\n          className=\"stroke-blue-500 dark:stroke-blue-400\"\n          fill=\"none\"\n          points=\"42,52 50,45 58,52\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"2\"\n        >\n          <animate\n            attributeName=\"points\"\n            dur=\"2s\"\n            repeatCount=\"indefinite\"\n            values=\"42,52 50,45 58,52;42,47 50,40 58,47;42,52 50,45 58,52\"\n          />\n        </polyline>\n      </g>\n    </svg>\n  </div>\n);\n\nconst UploadingAnimation = ({ progress }: { progress: number }) => (\n  <div className=\"relative h-16 w-16\">\n    <svg\n      aria-label={`Upload progress: ${Math.round(progress)}%`}\n      className=\"h-full w-full\"\n      fill=\"none\"\n      viewBox=\"0 0 240 240\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <title>Upload Progress Indicator</title>\n\n      <defs>\n        <mask id=\"progress-mask\">\n          <rect fill=\"black\" height=\"240\" width=\"240\" />\n          <circle\n            cx=\"120\"\n            cy=\"120\"\n            fill=\"white\"\n            r=\"120\"\n            strokeDasharray={`${(progress / 100) * 754}, 754`}\n            transform=\"rotate(-90 120 120)\"\n          />\n        </mask>\n      </defs>\n\n      <style>\n        {`\n                    @keyframes rotate-cw {\n                        from { transform: rotate(0deg); }\n                        to { transform: rotate(360deg); }\n                    }\n                    @keyframes rotate-ccw {\n                        from { transform: rotate(360deg); }\n                        to { transform: rotate(0deg); }\n                    }\n                    .g-spin circle {\n                        transform-origin: 120px 120px;\n                    }\n                    .g-spin circle:nth-child(1) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(2) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(3) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(4) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(5) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(6) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(7) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(8) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(9) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(10) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(11) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(12) { animation: rotate-ccw 8s linear infinite; }\n                    .g-spin circle:nth-child(13) { animation: rotate-cw 8s linear infinite; }\n                    .g-spin circle:nth-child(14) { animation: rotate-ccw 8s linear infinite; }\n\n                    .g-spin circle:nth-child(2n) { animation-delay: 0.2s; }\n                    .g-spin circle:nth-child(3n) { animation-delay: 0.3s; }\n                    .g-spin circle:nth-child(5n) { animation-delay: 0.5s; }\n                    .g-spin circle:nth-child(7n) { animation-delay: 0.7s; }\n                `}\n      </style>\n\n      <g\n        className=\"g-spin\"\n        mask=\"url(#progress-mask)\"\n        strokeDasharray=\"18% 40%\"\n        strokeWidth=\"10\"\n      >\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"150\" stroke=\"#FF2E7E\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"140\" stroke=\"#FFD600\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"130\" stroke=\"#00E5FF\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"120\" stroke=\"#FF3D71\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"110\" stroke=\"#4ADE80\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"100\" stroke=\"#2196F3\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"90\" stroke=\"#FFA726\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"80\" stroke=\"#FF1493\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"70\" stroke=\"#FFEB3B\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"60\" stroke=\"#00BCD4\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"50\" stroke=\"#FF4081\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"40\" stroke=\"#76FF03\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"30\" stroke=\"#448AFF\" />\n        <circle cx=\"120\" cy=\"120\" opacity=\"0.95\" r=\"20\" stroke=\"#FF3D00\" />\n      </g>\n    </svg>\n  </div>\n);\n\nexport default function FileUpload({\n  onUploadSuccess = () => {},\n  onUploadError = () => {},\n  acceptedFileTypes = [],\n  maxFileSize = DEFAULT_MAX_FILE_SIZE,\n  currentFile: initialFile = null,\n  onFileRemove = () => {},\n  uploadDelay = 2000,\n  validateFile = () => null,\n  className,\n}: FileUploadProps) {\n  const [file, setFile] = useState<File | null>(initialFile);\n  const [status, setStatus] = useState<FileStatus>(\"idle\");\n  const [progress, setProgress] = useState(0);\n  const [error, setError] = useState<FileError | null>(null);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const uploadIntervalRef = useRef<NodeJS.Timeout | null>(null);\n\n  useEffect(\n    () => () => {\n      if (uploadIntervalRef.current) {\n        clearInterval(uploadIntervalRef.current);\n      }\n    },\n    []\n  );\n\n  const validateFileSize = useCallback(\n    (file: File): FileError | null => {\n      if (file.size > maxFileSize) {\n        return {\n          message: `File size exceeds ${formatBytes(maxFileSize)}`,\n          code: \"FILE_TOO_LARGE\",\n        };\n      }\n      return null;\n    },\n    [maxFileSize]\n  );\n\n  const validateFileType = useCallback(\n    (file: File): FileError | null => {\n      if (!acceptedFileTypes?.length) return null;\n\n      const fileType = file.type.toLowerCase();\n      if (\n        !acceptedFileTypes.some((type) => fileType.match(type.toLowerCase()))\n      ) {\n        return {\n          message: `File type must be ${acceptedFileTypes.join(\", \")}`,\n          code: \"INVALID_FILE_TYPE\",\n        };\n      }\n      return null;\n    },\n    [acceptedFileTypes]\n  );\n\n  const handleError = useCallback(\n    (error: FileError) => {\n      setError(error);\n      setStatus(\"error\");\n      onUploadError?.(error);\n\n      setTimeout(() => {\n        setError(null);\n        setStatus(\"idle\");\n      }, 3000);\n    },\n    [onUploadError]\n  );\n\n  const simulateUpload = useCallback(\n    (uploadingFile: File) => {\n      let currentProgress = 0;\n\n      if (uploadIntervalRef.current) {\n        clearInterval(uploadIntervalRef.current);\n      }\n\n      uploadIntervalRef.current = setInterval(\n        () => {\n          currentProgress += UPLOAD_STEP_SIZE;\n          if (currentProgress >= 100) {\n            if (uploadIntervalRef.current) {\n              clearInterval(uploadIntervalRef.current);\n            }\n            setProgress(0);\n            setStatus(\"idle\");\n            setFile(null);\n            onUploadSuccess?.(uploadingFile);\n          } else {\n            setStatus((prevStatus) => {\n              if (prevStatus === \"uploading\") {\n                setProgress(currentProgress);\n                return \"uploading\";\n              }\n              if (uploadIntervalRef.current) {\n                clearInterval(uploadIntervalRef.current);\n              }\n              return prevStatus;\n            });\n          }\n        },\n        uploadDelay / (100 / UPLOAD_STEP_SIZE)\n      );\n    },\n    [onUploadSuccess, uploadDelay]\n  );\n\n  const handleFileSelect = useCallback(\n    (selectedFile: File | null) => {\n      if (!selectedFile) return;\n\n      // Reset error state\n      setError(null);\n\n      // Validate file\n      const sizeError = validateFileSize(selectedFile);\n      if (sizeError) {\n        handleError(sizeError);\n        return;\n      }\n\n      const typeError = validateFileType(selectedFile);\n      if (typeError) {\n        handleError(typeError);\n        return;\n      }\n\n      const customError = validateFile?.(selectedFile);\n      if (customError) {\n        handleError(customError);\n        return;\n      }\n\n      setFile(selectedFile);\n      setStatus(\"uploading\");\n      setProgress(0);\n      simulateUpload(selectedFile);\n    },\n    [\n      simulateUpload,\n      validateFileSize,\n      validateFileType,\n      validateFile,\n      handleError,\n    ]\n  );\n\n  const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setStatus((prev) => (prev !== \"uploading\" ? \"dragging\" : prev));\n  }, []);\n\n  const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setStatus((prev) => (prev === \"dragging\" ? \"idle\" : prev));\n  }, []);\n\n  const handleDrop = useCallback(\n    (e: DragEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      e.stopPropagation();\n      if (status === \"uploading\") return;\n      setStatus(\"idle\");\n      const droppedFile = e.dataTransfer.files?.[0];\n      if (droppedFile) handleFileSelect(droppedFile);\n    },\n    [status, handleFileSelect]\n  );\n\n  const handleFileInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const selectedFile = e.target.files?.[0];\n      handleFileSelect(selectedFile || null);\n      if (e.target) e.target.value = \"\";\n    },\n    [handleFileSelect]\n  );\n\n  const triggerFileInput = useCallback(() => {\n    if (status === \"uploading\") return;\n    fileInputRef.current?.click();\n  }, [status]);\n\n  const resetState = useCallback(() => {\n    setFile(null);\n    setStatus(\"idle\");\n    setProgress(0);\n    if (onFileRemove) onFileRemove();\n  }, [onFileRemove]);\n\n  return (\n    <div\n      aria-label=\"File upload\"\n      className={cn(\"relative mx-auto w-full max-w-sm\", className || \"\")}\n      role=\"complementary\"\n    >\n      <div className=\"group relative w-full rounded-xl bg-white p-0.5 ring-1 ring-gray-200 dark:bg-black dark:ring-white/10\">\n        <div className=\"absolute inset-x-0 -top-px h-px w-full bg-gradient-to-r from-transparent via-blue-500/20 to-transparent\" />\n\n        <div className=\"relative w-full rounded-[10px] bg-gray-50/50 p-1.5 dark:bg-white/[0.02]\">\n          <div\n            className={cn(\n              \"relative mx-auto w-full overflow-hidden rounded-lg border border-gray-100 bg-white dark:border-white/[0.08] dark:bg-black/50\",\n              error ? \"border-red-500/50\" : \"\"\n            )}\n          >\n            <div\n              className={cn(\n                \"absolute inset-0 transition-opacity duration-300\",\n                status === \"dragging\" ? \"opacity-100\" : \"opacity-0\"\n              )}\n            >\n              <div className=\"absolute inset-x-0 top-0 h-[20%] bg-gradient-to-b from-blue-500/10 to-transparent\" />\n              <div className=\"absolute inset-x-0 bottom-0 h-[20%] bg-gradient-to-t from-blue-500/10 to-transparent\" />\n              <div className=\"absolute inset-y-0 left-0 w-[20%] bg-gradient-to-r from-blue-500/10 to-transparent\" />\n              <div className=\"absolute inset-y-0 right-0 w-[20%] bg-gradient-to-l from-blue-500/10 to-transparent\" />\n              <div className=\"absolute inset-[20%] animate-pulse rounded-lg bg-blue-500/5 transition-all duration-300\" />\n            </div>\n\n            <div className=\"absolute -top-4 -right-4 h-8 w-8 bg-gradient-to-br from-blue-500/20 to-transparent opacity-0 blur-md transition-opacity duration-500 group-hover:opacity-100\" />\n\n            <div className=\"relative h-[240px]\">\n              <AnimatePresence mode=\"wait\">\n                {status === \"idle\" || status === \"dragging\" ? (\n                  <motion.div\n                    animate={{\n                      opacity: status === \"dragging\" ? 0.8 : 1,\n                      y: 0,\n                      scale: status === \"dragging\" ? 0.98 : 1,\n                    }}\n                    className=\"absolute inset-0 flex flex-col items-center justify-center p-6\"\n                    exit={{ opacity: 0, y: -10 }}\n                    initial={{ opacity: 0, y: 10 }}\n                    key=\"dropzone\"\n                    onDragLeave={handleDragLeave}\n                    onDragOver={handleDragOver}\n                    onDrop={handleDrop}\n                    transition={{ duration: 0.2 }}\n                  >\n                    <div className=\"mb-4\">\n                      <UploadIllustration />\n                    </div>\n\n                    <div className=\"mb-4 space-y-1.5 text-center\">\n                      <h3 className=\"font-semibold text-gray-900 text-lg tracking-tight dark:text-white\">\n                        Drag and drop or\n                      </h3>\n                      <p className=\"text-gray-500 text-xs dark:text-gray-400\">\n                        {acceptedFileTypes?.length\n                          ? `${acceptedFileTypes\n                              .map((t) => t.split(\"/\")[1])\n                              .join(\", \")\n                              .toUpperCase()}`\n                          : \"SVG, PNG, JPG or GIF\"}{\" \"}\n                        {maxFileSize && `up to ${formatBytes(maxFileSize)}`}\n                      </p>\n                    </div>\n\n                    <button\n                      className=\"group flex w-4/5 items-center justify-center gap-2 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-900 text-sm transition-all duration-200 hover:bg-gray-200 dark:bg-white/10 dark:text-white dark:hover:bg-white/20\"\n                      onClick={triggerFileInput}\n                      type=\"button\"\n                    >\n                      <span>Upload File</span>\n                      <UploadCloud className=\"h-4 w-4 transition-transform duration-200 group-hover:scale-110\" />\n                    </button>\n\n                    <p className=\"mt-3 text-gray-500 text-xs dark:text-gray-400\">\n                      or drag and drop your file here\n                    </p>\n\n                    <input\n                      accept={acceptedFileTypes?.join(\",\")}\n                      aria-label=\"File input\"\n                      className=\"sr-only\"\n                      onChange={handleFileInputChange}\n                      ref={fileInputRef}\n                      type=\"file\"\n                    />\n                  </motion.div>\n                ) : status === \"uploading\" ? (\n                  <motion.div\n                    animate={{ opacity: 1, scale: 1 }}\n                    className=\"absolute inset-0 flex flex-col items-center justify-center p-6\"\n                    exit={{ opacity: 0, scale: 0.95 }}\n                    initial={{ opacity: 0, scale: 0.95 }}\n                    key=\"uploading\"\n                  >\n                    <div className=\"mb-4\">\n                      <UploadingAnimation progress={progress} />\n                    </div>\n\n                    <div className=\"mb-4 space-y-1.5 text-center\">\n                      <h3 className=\"truncate font-semibold text-gray-900 text-sm dark:text-white\">\n                        {file?.name}\n                      </h3>\n                      <div className=\"flex items-center justify-center gap-2 text-xs\">\n                        <span className=\"text-gray-500 dark:text-gray-400\">\n                          {formatBytes(file?.size || 0)}\n                        </span>\n                        <span className=\"font-medium text-blue-500\">\n                          {Math.round(progress)}%\n                        </span>\n                      </div>\n                    </div>\n\n                    <button\n                      className=\"flex w-4/5 items-center justify-center gap-2 rounded-lg bg-gray-100 px-4 py-2.5 font-semibold text-gray-900 text-sm transition-all duration-200 hover:bg-gray-200 dark:bg-white/10 dark:text-white dark:hover:bg-white/20\"\n                      onClick={resetState}\n                      type=\"button\"\n                    >\n                      Cancel\n                    </button>\n                  </motion.div>\n                ) : null}\n              </AnimatePresence>\n            </div>\n\n            <AnimatePresence>\n              {error && (\n                <motion.div\n                  animate={{ opacity: 1, y: 0 }}\n                  className=\"absolute bottom-4 left-1/2 -translate-x-1/2 transform rounded-lg border border-red-500/20 bg-red-500/10 px-4 py-2\"\n                  exit={{ opacity: 0, y: -10 }}\n                  initial={{ opacity: 0, y: 10 }}\n                >\n                  <p className=\"text-red-500 text-sm dark:text-red-400\">\n                    {error.message}\n                  </p>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nFileUpload.displayName = \"FileUpload\";\n",
      "path": "/components/kokonutui/file-upload.tsx",
      "target": "components/kokonutui/file-upload.tsx"
    }
  ]
}