copy.tsx110 lines · main
1'use client';
2
3import type { Transition } from 'motion/react';
4import { motion, useAnimation } from 'motion/react';
5import type { HTMLAttributes } from 'react';
6import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
7
8import { cn } from '@/lib/utils';
9
10export interface CopyIconHandle {
11 startAnimation: () => void;
12 stopAnimation: () => void;
13}
14
15interface CopyIconProps extends HTMLAttributes<HTMLDivElement> {
16 size?: number;
17}
18
19const DEFAULT_TRANSITION: Transition = {
20 type: 'spring',
21 stiffness: 160,
22 damping: 17,
23 mass: 1,
24};
25
26const CopyIcon = forwardRef<CopyIconHandle, CopyIconProps>(
27 ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
28 const controls = useAnimation();
29 const isControlledRef = useRef(false);
30
31 useImperativeHandle(ref, () => {
32 isControlledRef.current = true;
33
34 return {
35 startAnimation: () => controls.start('animate'),
36 stopAnimation: () => controls.start('normal'),
37 };
38 });
39
40 const handleMouseEnter = useCallback(
41 (e: React.MouseEvent<HTMLDivElement>) => {
42 if (isControlledRef.current) {
43 onMouseEnter?.(e);
44 } else {
45 controls.start('animate');
46 }
47 },
48 [controls, onMouseEnter],
49 );
50
51 const handleMouseLeave = useCallback(
52 (e: React.MouseEvent<HTMLDivElement>) => {
53 if (isControlledRef.current) {
54 onMouseLeave?.(e);
55 } else {
56 controls.start('normal');
57 }
58 },
59 [controls, onMouseLeave],
60 );
61 return (
62 <div
63 className={cn(className)}
64 onMouseEnter={handleMouseEnter}
65 onMouseLeave={handleMouseLeave}
66 {...props}
67 >
68 <svg
69 fill="none"
70 height={size}
71 stroke="currentColor"
72 strokeLinecap="round"
73 strokeLinejoin="round"
74 strokeWidth="2"
75 viewBox="0 0 24 24"
76 width={size}
77 xmlns="http://www.w3.org/2000/svg"
78 >
79 <motion.rect
80 animate={controls}
81 height="14"
82 rx="2"
83 ry="2"
84 transition={DEFAULT_TRANSITION}
85 variants={{
86 normal: { translateY: 0, translateX: 0 },
87 animate: { translateY: -3, translateX: -3 },
88 }}
89 width="14"
90 x="8"
91 y="8"
92 />
93 <motion.path
94 animate={controls}
95 d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
96 transition={DEFAULT_TRANSITION}
97 variants={{
98 normal: { x: 0, y: 0 },
99 animate: { x: 3, y: 3 },
100 }}
101 />
102 </svg>
103 </div>
104 );
105 },
106);
107
108CopyIcon.displayName = 'CopyIcon';
109
110export { CopyIcon };