INFINITYUI
HomeComponentsDocsTemplates
Star

Getting Started

  • Introduction
  • Installation
  • CLI
  • Audit

Navigation

  • Spotlight Navbar
  • Glass DockNEW
  • Animated Tab Bar
  • Circle Menu
  • Magnet Tabs
  • Animated Sidebar
  • Apple Spotlight
  • Page TOC RailNEW

Text

  • Flip Text
  • Glitch Text
  • Liquid Text
  • Flip Fade Text
  • Mask Cursor Effect

Cards

  • Glow Border Card
  • Testimonials Card
  • Interactive Book
  • Trading CardsNEW
  • Hover Image
  • Chain of ThoughtNEW
  • Masonry Grid
  • Image Pile
  • Staggered Grid

Inputs

  • AI InputNEW
  • OTP Input
  • Leave Rating

Buttons

  • Social Flip Button
  • Creepy Button

Loaders

  • Jelly Loader
  • Rolling Ball Scroll
  • Glowing Scroll

Backgrounds

  • Light Lines
  • Perspective Grid
  • Liquid Ocean
  • Eagle Vision
  • Flow Scroll
  • Horizontal Scroll

Overlays

  • PersonaNEW
  • Infinite Moving Cards
  • Masked Avatars
  • Stacked Logos
  • Icon Wheel
  • Pixelated CarouselNEW
  • Pixelated Image Trail
  • Flip Scroll
  • Interactive Folder
  • Animated Folder IconNEW
  • Stack Scroll
  • Rubik Cube
OverlaysNEW

Animated Folder Icon

#folder#documents#hover

Documents

originalInfinity original

This is a newly added InfinityUI-only component and is not replacing any legacy component.

Installation

Use the registry command to add the component source, and install any package dependencies if needed.

Infinity Registry

Install the component source into your project with the shadcn CLI.

npx shadcn@latest add https://infinityui-pearl.vercel.app/r/animated-folder-icon
Package Dependencies

Install the npm packages used by this component source.

npm install clsx framer-motion tailwind-merge

Component Code

Copy and paste this code into your component file.

tsx
"use client";

import { useState, type ComponentPropsWithoutRef } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";

type MotionTarget = {
    y: number;
    x: number;
    rotate: number;
    zIndex: number;
};

type CardPosition = "left" | "middle" | "right";

export interface AnimatedFolderIconProps
    extends ComponentPropsWithoutRef<"div"> {
    label?: string;
    cardCount?: 1 | 2 | 3;
}

type SkeletonCardProps = {
    animate: MotionTarget;
    initial: MotionTarget;
    transition: typeof springTransition;
};

function SkeletonCard({
    animate,
    initial,
    transition,
}: SkeletonCardProps) {
    return (
        <motion.div
            initial={initial}
            animate={animate}
            transition={transition}
            className="absolute bottom-0 flex h-36 w-28 origin-bottom flex-col gap-2 rounded-lg border border-gray-200 bg-white p-3 shadow-md"
        >
            <div className="h-3 w-full rounded bg-gray-200" />
            <div className="h-2 w-3/4 rounded bg-gray-100" />
            <div className="mt-1 h-px w-full bg-gray-100" />
            <div className="mt-1 flex flex-col gap-1.5">
                <div className="h-2 w-full rounded bg-gray-100" />
                <div className="h-2 w-5/6 rounded bg-gray-100" />
                <div className="h-2 w-4/6 rounded bg-gray-100" />
            </div>
            <div className="mt-auto flex items-center gap-1.5">
                <div className="size-4 rounded-full bg-gray-200" />
                <div className="h-2 flex-1 rounded bg-gray-100" />
            </div>
        </motion.div>
    );
}

function FolderBack() {
    return (
        <div className="absolute inset-0 flex flex-col">
            <div
                className="ml-4 self-start rounded-t-md"
                style={{
                    width: 72,
                    height: 14,
                    background: "linear-gradient(135deg, #1a1a1f 0%, #25252d 100%)",
                }}
            />
            <div
                className="flex-1 rounded-b-xl rounded-tr-xl"
                style={{
                    background: "linear-gradient(160deg, #1c1c24 0%, #14141a 100%)",
                    boxShadow: "0 6px 24px rgba(0,0,0,0.5)",
                }}
            />
        </div>
    );
}

function FolderFront() {
    return (
        <div
            className="absolute bottom-0 left-0 right-0 rounded-xl"
            style={{
                height: "72%",
                background:
                    "linear-gradient(160deg, #26262f 0%, #1a1a22 60%, #111117 100%)",
                boxShadow:
                    "0 -2px 0 0 rgba(255,255,255,0.06) inset, 0 8px 32px rgba(0,0,0,0.6)",
                borderTop: "1px solid rgba(255,255,255,0.08)",
            }}
        >
            <div
                className="absolute left-6 right-6 top-0 h-px rounded-full"
                style={{
                    background:
                        "linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent)",
                }}
            />
            <div className="absolute bottom-3 right-4 flex gap-1.5">
                {[0, 1, 2].map((dot) => (
                    <div
                        key={dot}
                        className="size-1 rounded-full"
                        style={{ background: "rgba(255,255,255,0.12)" }}
                    />
                ))}
            </div>
        </div>
    );
}

const springTransition = { type: "spring" as const, stiffness: 300, damping: 20 };

const cardVariants: Record<
    CardPosition,
    { default: MotionTarget; hover: MotionTarget }
> = {
    left: {
        default: { y: 0, x: 0, rotate: 0, zIndex: 10 },
        hover: { y: -50, x: -40, rotate: -12, zIndex: 10 },
    },
    middle: {
        default: { y: 0, x: 0, rotate: 0, zIndex: 11 },
        hover: { y: -60, x: 0, rotate: 0, zIndex: 11 },
    },
    right: {
        default: { y: 0, x: 0, rotate: 0, zIndex: 10 },
        hover: { y: -50, x: 40, rotate: 12, zIndex: 10 },
    },
};

export function AnimatedFolderIcon({
    label = "Documents",
    cardCount = 3,
    className,
    ...props
}: AnimatedFolderIconProps) {
    const [hovered, setHovered] = useState(false);

    return (
        <div
            className={cn("relative inline-flex flex-col items-center", className)}
            {...props}
        >
            <motion.div
                animate={hovered ? { opacity: 1 } : { opacity: 0 }}
                transition={{ duration: 0.3 }}
                className="pointer-events-none absolute rounded-full"
                style={{
                    width: 200,
                    height: 200,
                    background:
                        "radial-gradient(circle, rgba(120,100,255,0.12) 0%, transparent 70%)",
                    filter: "blur(20px)",
                }}
            />

            <motion.div
                onHoverStart={() => setHovered(true)}
                onHoverEnd={() => setHovered(false)}
                animate={hovered ? "hover" : "default"}
                variants={{
                    default: { rotateX: 0, scale: 1 },
                    hover: { rotateX: 4, scale: 1.03 },
                }}
                transition={springTransition}
                style={{ perspective: 800, cursor: "pointer" }}
                className="relative select-none"
                aria-label={`${label} animated folder icon`}
            >
                <div className="relative" style={{ width: 160, height: 130 }}>
                    <FolderBack />

                    <div
                        className="absolute inset-x-0 flex justify-center"
                        style={{ bottom: "30%", zIndex: 5 }}
                    >
                        {cardCount >= 1 && (
                            <SkeletonCard
                                initial={cardVariants.left.default}
                                animate={
                                    hovered
                                        ? cardVariants.left.hover
                                        : cardVariants.left.default
                                }
                                transition={springTransition}
                            />
                        )}

                        {cardCount >= 2 && (
                            <SkeletonCard
                                initial={cardVariants.right.default}
                                animate={
                                    hovered
                                        ? cardVariants.right.hover
                                        : cardVariants.right.default
                                }
                                transition={springTransition}
                            />
                        )}

                        <SkeletonCard
                            initial={cardVariants.middle.default}
                            animate={
                                hovered
                                    ? cardVariants.middle.hover
                                    : cardVariants.middle.default
                            }
                            transition={springTransition}
                        />
                    </div>

                    <div className="absolute inset-0" style={{ zIndex: 20 }}>
                        <FolderFront />
                    </div>
                </div>

                <motion.p
                    animate={hovered ? { opacity: 1, y: 0 } : { opacity: 0.4, y: 2 }}
                    transition={{ duration: 0.25 }}
                    className="mt-5 text-center text-xs font-medium uppercase tracking-[0.18em]"
                    style={{
                        color: "rgba(255,255,255,0.55)",
                        fontFamily: "'DM Mono', 'Courier New', monospace",
                    }}
                >
                    {label}
                </motion.p>
            </motion.div>
        </div>
    );
}

export default AnimatedFolderIcon;