{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "folder-animation", "type": "registry:ui", "title": "Folder Animation", "description": "Animated folder card with motion blur effect, perfect for showcasing file operations or loading states", "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-carbon/src/components/native/folder-animation-carbon.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n motion,\n useAnimationControls,\n useReducedMotion,\n} from \"framer-motion\";\nimport { type ReactNode, useState, useCallback, useEffect, useRef } from \"react\";\n\ninterface FolderAnimationProps {\n /**\n * Duration of the animation cycle in seconds.\n * Default: 3\n */\n animationDuration?: number;\n /**\n * Custom folder icon to display. If not provided, a default folder icon is rendered.\n */\n folderIcon?: ReactNode;\n /**\n * Content to display in the card footer.\n */\n children?: ReactNode;\n /**\n * Additional CSS classes for the container.\n */\n className?: string;\n /**\n * Accessible label for the animation area.\n */\n ariaLabel?: string;\n}\n\nexport function FolderAnimation({\n animationDuration = 3,\n folderIcon,\n children,\n className,\n ariaLabel = \"Animated folder illustration\",\n}: FolderAnimationProps) {\n const [isHovered, setIsHovered] = useState(false);\n const prefersReducedMotion = useReducedMotion();\n const controls = useAnimationControls();\n const blurControls = useAnimationControls();\n const isAnimatingRef = useRef(false);\n const shouldStopRef = useRef(false);\n\n const handleMouseEnter = useCallback(() => setIsHovered(true), []);\n const handleMouseLeave = useCallback(() => setIsHovered(false), []);\n const handleFocus = useCallback(() => setIsHovered(true), []);\n const handleBlur = useCallback(() => setIsHovered(false), []);\n\n // Animation sequence: center → right → (wrap to left) → center → repeat\n const runAnimation = useCallback(async () => {\n if (prefersReducedMotion) return;\n\n isAnimatingRef.current = true;\n shouldStopRef.current = false;\n\n while (!shouldStopRef.current) {\n // Phase 1: Center to right (exit right with blur)\n await Promise.all([\n controls.start({\n x: \"150%\",\n opacity: [1, 1, 0],\n transition: {\n duration: animationDuration / 2,\n ease: \"easeIn\",\n times: [0, 0.7, 1],\n },\n }),\n blurControls.start({\n filter: [\"blur(0px)\", \"blur(0px)\", \"blur(8px)\", \"blur(16px)\"],\n transition: {\n duration: animationDuration / 2,\n ease: \"easeIn\",\n times: [0, 0.5, 0.75, 1],\n },\n }),\n ]);\n\n if (shouldStopRef.current) break;\n\n // Instantly move to left (off-screen)\n await controls.set({ x: \"-150%\", opacity: 0 });\n await blurControls.set({ filter: \"blur(16px)\" });\n\n if (shouldStopRef.current) break;\n\n // Phase 2: Left to center (enter from left, deblur)\n await Promise.all([\n controls.start({\n x: \"0%\",\n opacity: [0, 1, 1],\n transition: {\n duration: animationDuration / 2,\n ease: \"easeOut\",\n times: [0, 0.3, 1],\n },\n }),\n blurControls.start({\n filter: [\"blur(16px)\", \"blur(8px)\", \"blur(0px)\", \"blur(0px)\"],\n transition: {\n duration: animationDuration / 2,\n ease: \"easeOut\",\n times: [0, 0.25, 0.5, 1],\n },\n }),\n ]);\n\n if (shouldStopRef.current) break;\n\n // Brief pause at center\n await new Promise((resolve) => setTimeout(resolve, 400));\n }\n\n isAnimatingRef.current = false;\n }, [controls, blurControls, animationDuration, prefersReducedMotion]);\n\n // Return to center smoothly\n const returnToCenter = useCallback(async () => {\n shouldStopRef.current = true;\n\n await Promise.all([\n controls.start({\n x: \"0%\",\n opacity: 1,\n transition: {\n duration: 0.5,\n ease: [0.25, 0.1, 0.25, 1], // cubic-bezier for smooth deceleration\n },\n }),\n blurControls.start({\n filter: \"blur(0px)\",\n transition: {\n duration: 0.3,\n ease: \"easeOut\",\n },\n }),\n ]);\n\n isAnimatingRef.current = false;\n }, [controls, blurControls]);\n\n useEffect(() => {\n if (isHovered && !prefersReducedMotion) {\n runAnimation();\n } else {\n returnToCenter();\n }\n }, [isHovered, prefersReducedMotion, runAnimation, returnToCenter]);\n\n // Set initial position\n useEffect(() => {\n controls.set({ x: \"0%\", opacity: 1 });\n blurControls.set({ filter: \"blur(0px)\" });\n }, [controls, blurControls]);\n\n return (\n
\n {/* Card Container */}\n
\n {/* Animation Area */}\n \n {/* Animated Icon/Folder */}\n \n \n {folderIcon ? (\n
{folderIcon}
\n ) : (\n
\n {/* Folder Back */}\n
\n {/* Tab */}\n
\n\n {/* Files inside */}\n
\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n
\n
\n
\n\n {/* Folder Front */}\n
\n
\n
\n
\n )}\n \n \n
\n\n {/* Card Footer */}\n {children &&
{children}
}\n
\n
\n );\n}\n", "type": "registry:ui", "target": "components/uitripled/folder-animation-carbon.tsx" } ] }