file-upload-motion

PreviousNext

A file upload component with drag and drop animation.

Docs
tailwind-admincomponent

Preview

Loading preview…
app/components/animatedComponents/forms/file-upload/FileUploadMotion.tsx
import { cn } from "@/lib/utils";
import React, { useRef, useState } from "react";
import { motion } from "motion/react";
import { IconUpload } from "@tabler/icons-react";
import { useDropzone } from "react-dropzone";

const mainVariant = {
  initial: { x: 0, y: 0 },
  animate: { x: 20, y: -20, opacity: 0.9 },
};

const secondaryVariant = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
};

interface FileUploadProps {
  onChange?: (files: File[]) => void;
}

export const FileUploadStruc: React.FC<FileUploadProps> = ({ onChange }) => {
  const [files, setFiles] = useState<File[]>([]);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (newFiles: File[]) => {
    setFiles((prev) => [...prev, ...newFiles]);
    onChange?.(newFiles);
  };

  const handleClick = () => {
    fileInputRef.current?.click();
  };

  const { getRootProps, isDragActive } = useDropzone({
    multiple: false,
    noClick: true,
    onDrop: handleFileChange,
    onDropRejected: console.error,
  });

  const formatFileSize = (size: number) =>
    (size / (1024 * 1024)).toFixed(2);

  const formatDate = (timestamp: number) =>
    new Date(timestamp).toLocaleDateString();

  return (
    <div className="w-full" {...getRootProps()}>
      <motion.div
        onClick={handleClick}
        whileHover="animate"
        className="p-6 group/file block rounded-lg cursor-pointer w-full relative overflow-hidden"
      >
        <input
          ref={fileInputRef}
          id="file-upload-handle"
          type="file"
          onChange={(e) => handleFileChange(Array.from(e.target.files || []))}
          className="hidden"
        />
        <div className="flex flex-col items-center justify-center">
          <p className="relative z-20 font-bold text-dark dark:text-neutral-300 text-xl">
            Upload file
          </p>
          <p className="relative z-20 font-normal text-neutral-400 dark:text-neutral-400 text-base mt-2">
            Drag or drop your files here or click to upload
          </p>
          <div className="relative w-full mt-10 max-w-xl mx-auto">
            {files.length > 0 ? (
              files.map((file, idx) => (
                <FileItem
                  key={file.name + idx}
                  file={file}
                  formatFileSize={formatFileSize}
                  formatDate={formatDate}
                  isFirst={idx === 0}
                />
              ))
            ) : (
              <EmptyState isDragActive={isDragActive} />
            )}
          </div>
        </div>
      </motion.div>
    </div>
  );
};

// File Item Component
interface FileItemProps {
  file: File;
  formatFileSize: (size: number) => string;
  formatDate: (timestamp: number) => string;
  isFirst: boolean;
}
const FileItem: React.FC<FileItemProps> = ({
  file,
  formatFileSize,
  formatDate,
  isFirst,
}) => (
  <motion.div
    layoutId={isFirst ? "file-upload" : `file-upload-${file.name}`}
    className={cn(
      "relative overflow-hidden z-40 bg-white dark:bg-white/10 flex flex-col items-start justify-start md:h-24 p-4 mt-4 w-full mx-auto rounded-md shadow-sm"
    )}
  >
    <div className="flex justify-between w-full items-center gap-4">
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        layout
        className="text-sm font-medium text-neutral-700 dark:text-neutral-300 truncate max-w-xs"
      >
        {file.name}
      </motion.p>
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        layout
        className="rounded-lg px-2 py-1 w-fit shrink-0 text-xs font-medium text-neutral-600 dark:bg-white/20 dark:text-white shadow-input"
      >
        {formatFileSize(file.size)} MB
      </motion.p>
    </div>
    <div className="flex text-sm md:flex-row flex-col items-start md:items-center w-full mt-2 justify-between text-neutral-600 dark:text-neutral-400">
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        layout
        className="px-3 py-1 rounded-md bg-gray-100 dark:bg-white/20 text-xs"
      >
        {file.type}
      </motion.p>
      <motion.p
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        layout
        className="text-sm"
      >
        modified {formatDate(file.lastModified)}
      </motion.p>
    </div>
  </motion.div>
);

// Empty State Component
interface EmptyStateProps {
  isDragActive: boolean;
}
const EmptyState: React.FC<EmptyStateProps> = ({ isDragActive }) => (
  <>
    <motion.div
      layoutId="file-upload"
      variants={mainVariant}
      transition={{
        type: "spring",
        stiffness: 300,
        damping: 20,
      }}
      className={cn(
        "relative group-hover/file:shadow-2xl z-40 bg-white dark:bg-white/10 flex items-center justify-center h-28 mt-4 w-full max-w-[8rem] mx-auto rounded-md shadow-[0px_10px_50px_rgba(0,0,0,0.1)]"
      )}
    >
      {isDragActive ? (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className="text-neutral-600 flex flex-col items-center"
        >
          Drop it
          <IconUpload className="h-6 w-6 text-neutral-600 dark:text-neutral-400" />
        </motion.p>
      ) : (
        <IconUpload className="h-6 w-6 text-neutral-600 dark:text-neutral-300" />
      )}
    </motion.div>
    <motion.div
      variants={secondaryVariant}
      className="absolute opacity-0 border border-dashed border-primary inset-0 z-30 bg-transparent flex items-center justify-center h-28 mt-4 w-full max-w-[8rem] mx-auto rounded-md"
    />
  </>
);


export default function FileUploadMotion(){
  const [files, setFiles] = useState<File[]>([]);
  const handleFileUpload = (files: File[]) => {
    setFiles(files);
  };
    return (
        <>
<div className="w-full max-w-md mx-auto min-h-72 border border-dashed bg-white dark:bg-forest-black border-border rounded-lg">
      <FileUploadStruc onChange={handleFileUpload} />
    </div>
        </>
    )
}


Installation

npx shadcn@latest add @tailwind-admin/file-upload-motion

Usage

import { FileUploadMotion } from "@/components/file-upload-motion"
<FileUploadMotion />