Interactive Folder

An animated folder component that expands to reveal file contents with smooth spring animations and interactive hover effects.

Install dependencies

npm i framer-motion lucide-react

Component

Create a file interactive-folder.tsx in your components folder and paste this code

'use client'; import { AnimatePresence, motion } from 'framer-motion'; import { X } from 'lucide-react'; import Image from 'next/image'; import React, { useState } from 'react'; const DUMMY_FILES = [ { id: 1, name: 'assignment.doc', image: '/fileicons/DOC.svg' }, { id: 2, name: 'me.jpg', image: '/fileicons/JPG.svg' }, { id: 3, name: 'johnmayer.mp3', image: '/fileicons/MP3.svg' }, { id: 4, name: 'bali.mp4', image: '/fileicons/MP4.svg' }, { id: 5, name: 'ebook.pdf', image: '/fileicons/PDF.svg' }, { id: 6, name: 'notes.txt', image: '/fileicons/TXT.svg' }, { id: 7, name: 'statement.xlsx', image: '/fileicons/XLSX.svg' }, { id: 8, name: 'export.zip', image: '/fileicons/ZIP.svg' } ]; const FolderIcon = () => ( <Image src="/folder.svg" alt="Folder Icon" width={64} height={64} className="w-full h-full" /> ); type FileIconProps = { size: number; image: string; }; const FileIcon = ({ size, image }: FileIconProps) => ( <Image src={image} alt="File Icon" width={size} height={size} /> ); const InteractiveFolder = ({ folderName = 'New Folder' }: { folderName?: string }) => { const [isOpen, setIsOpen] = useState(false); return ( <div className="flex items-center justify-center flex-col"> <motion.div initial={{ filter: 'blur(10px)', width: '50px', height: '50px' }} animate={{ height: isOpen ? '210px' : '50px', width: isOpen ? '350px' : '64px', backgroundColor: isOpen ? '#f1f1f1' : '#1BA3F8', borderRadius: isOpen ? '10px' : '10px', cursor: isOpen ? 'default' : 'pointer', filter: 'blur(0px)', boxShadow: isOpen ? '0px 5px 10px 0 rgba(0, 0, 0, 0.1)' : 'none' }} whileHover={{ boxShadow: !isOpen ? '0px 3px 10px 0 rgba(0, 0, 0, 0.25)' : '0px 5px 10px 0 rgba(0, 0, 0, 0.1)', rotateZ: !isOpen ? '-4deg' : '0deg', translateY: !isOpen ? '-3px' : '0px' }} whileTap={{ boxShadow: !isOpen ? '0px 0px 0px 0 rgba(0, 0, 0, 0.25)' : '0px 5px 10px 0 rgba(0, 0, 0, 0.1)', translateY: !isOpen ? '0px' : '0px', rotateZ: !isOpen ? '-2deg' : '0deg', scale: !isOpen ? 0.95 : 1 }} transition={{ type: 'spring', stiffness: 300, damping: 25 }} onClick={() => !isOpen && setIsOpen(!isOpen)} className="overflow-hidden" > <AnimatePresence mode="popLayout"> {!isOpen && ( <motion.div initial={{ filter: 'blur(10px)', opacity: 0 }} animate={{ filter: 'blur(0px)', opacity: 1 }} exit={{ filter: 'blur(10px)', opacity: 0 }} whileHover={{ translateY: '3px', rotateZ: '4deg' }} transition={{ type: 'spring', stiffness: 300, damping: 30 }} > <FolderIcon /> </motion.div> )} {isOpen && ( <motion.div initial={{ filter: 'blur(10px)', opacity: 0 }} animate={{ filter: 'blur(0px)', opacity: 1 }} exit={{ filter: 'blur(10px)', opacity: 0 }} transition={{ type: 'spring', stiffness: 300, damping: 30 }} className="h-full w-full flex flex-col overflow-hidden relative" > {/* folder container header */} <div className="h-7 w-full bg-white flex items-center justify-between"> <div className="bg-[#f1f1f1] h-full flex items-center justify-center"> <motion.p layout="position" layoutId="folder-name" className="text-black font-medium px-2 text-sm whitespace-nowrap truncate" > {folderName} </motion.p> </div> <div className="flex-1 flex items-center justify-end px-1 h-full rounded-bl-lg"> <button onClick={() => setIsOpen(false)} className="hover:bg-black/10 rounded-full cursor-pointer p-0.5" > <X className="text-black" size={14} /> </button> </div> </div> <div className="rounded-b-lg h-full w-full flex items-start justify-start"> <div style={{ gridTemplateColumns: 'repeat(4, 50px)' }} className="grid items-start justify-start gap-x-2 gap-y-4 overflow-y-scroll w-full p-2 py-4" > {DUMMY_FILES.map((file, index) => ( <motion.div key={'folder-item' + folderName + index} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ type: 'spring', stiffness: 300, damping: 30 }} className="w-[50px] h-[54px] gap-2 flex flex-col items-center justify-start overflow-hidden" > <FileIcon size={30} image={file.image} /> <p className="text-xs text-black truncate text-left w-full">{file.name}</p> </motion.div> ))} </div> </div> </motion.div> )} </AnimatePresence> </motion.div> {!isOpen && ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ type: 'spring', stiffness: 300, damping: 30 }} layoutId="folder-name" className="text-white font-medium text-sm whitespace-nowrap truncate" > {folderName} </motion.div> )} </div> ); }; export default InteractiveFolder;

Usage

<InteractiveFolder />
Folder Icon
New Folder