folders.tsx122 lines · main
1'use client';
2
3import { motion, useAnimation } from 'motion/react';
4import type { HTMLAttributes } from 'react';
5import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
6
7import { cn } from '@/lib/utils';
8
9export interface FoldersIconHandle {
10 startAnimation: () => void;
11 stopAnimation: () => void;
12}
13
14interface FoldersIconProps extends HTMLAttributes<HTMLDivElement> {
15 size?: number;
16}
17
18const FoldersIcon = forwardRef<FoldersIconHandle, FoldersIconProps>(
19 ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
20 const controls = useAnimation();
21 const isControlledRef = useRef(false);
22
23 useImperativeHandle(ref, () => {
24 isControlledRef.current = true;
25
26 return {
27 startAnimation: () => controls.start('animate'),
28 stopAnimation: () => controls.start('normal'),
29 };
30 });
31
32 const handleMouseEnter = useCallback(
33 (e: React.MouseEvent<HTMLDivElement>) => {
34 if (isControlledRef.current) {
35 onMouseEnter?.(e);
36 } else {
37 controls.start('animate');
38 }
39 },
40 [controls, onMouseEnter],
41 );
42
43 const handleMouseLeave = useCallback(
44 (e: React.MouseEvent<HTMLDivElement>) => {
45 if (isControlledRef.current) {
46 onMouseLeave?.(e);
47 } else {
48 controls.start('normal');
49 }
50 },
51 [controls, onMouseLeave],
52 );
53
54 return (
55 <div
56 className={cn(className)}
57 onMouseEnter={handleMouseEnter}
58 onMouseLeave={handleMouseLeave}
59 {...props}
60 >
61 <svg
62 fill="none"
63 height={size}
64 stroke="currentColor"
65 strokeLinecap="round"
66 strokeLinejoin="round"
67 strokeWidth="2"
68 viewBox="0 0 24 24"
69 width={size}
70 xmlns="http://www.w3.org/2000/svg"
71 >
72 <motion.path
73 animate={controls}
74 d="M20 17a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3.9a2 2 0 0 1-1.69-.9l-.81-1.2a2 2 0 0 0-1.67-.9H8a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2Z"
75 transition={{
76 type: 'spring',
77 stiffness: 250,
78 damping: 25,
79 }}
80 variants={{
81 normal: {
82 translateX: 0,
83 translateY: 0,
84 },
85 animate: {
86 translateX: -2,
87 translateY: 2,
88 },
89 }}
90 />
91 <motion.path
92 animate={controls}
93 d="M2 8v11a2 2 0 0 0 2 2h14"
94 transition={{
95 type: 'spring',
96 stiffness: 250,
97 damping: 25,
98 }}
99 variants={{
100 normal: {
101 translateX: 0,
102 translateY: 0,
103 opacity: 1,
104 scale: 1,
105 },
106 animate: {
107 translateX: 2,
108 translateY: -2,
109 opacity: 0,
110 scale: 0.9,
111 },
112 }}
113 />
114 </svg>
115 </div>
116 );
117 },
118);
119
120FoldersIcon.displayName = 'FoldersIcon';
121
122export { FoldersIcon };