chevrons-up-down.tsx103 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 ChevronsUpDownIconHandle {
11 startAnimation: () => void;
12 stopAnimation: () => void;
13}
14
15interface ChevronsUpDownIconProps extends HTMLAttributes<HTMLDivElement> {
16 size?: number;
17}
18
19const DEFAULT_TRANSITION: Transition = {
20 times: [0, 0.4, 1],
21 duration: 0.5,
22};
23
24const ChevronsUpDownIcon = forwardRef<ChevronsUpDownIconHandle, ChevronsUpDownIconProps>(
25 ({ onMouseEnter, onMouseLeave, className, size = 16, ...props }, ref) => {
26 const controls = useAnimation();
27 const isControlledRef = useRef(false);
28
29 useImperativeHandle(ref, () => {
30 isControlledRef.current = true;
31 return {
32 startAnimation: () => controls.start('animate'),
33 stopAnimation: () => controls.start('normal'),
34 };
35 });
36
37 const handleMouseEnter = useCallback(
38 (e: React.MouseEvent<HTMLDivElement>) => {
39 if (isControlledRef.current) {
40 onMouseEnter?.(e);
41 } else {
42 controls.start('animate');
43 }
44 },
45 [controls, onMouseEnter],
46 );
47
48 const handleMouseLeave = useCallback(
49 (e: React.MouseEvent<HTMLDivElement>) => {
50 if (isControlledRef.current) {
51 onMouseLeave?.(e);
52 } else {
53 controls.start('normal');
54 }
55 },
56 [controls, onMouseLeave],
57 );
58
59 return (
60 <div
61 className={cn(className)}
62 onMouseEnter={handleMouseEnter}
63 onMouseLeave={handleMouseLeave}
64 {...props}
65 >
66 <svg
67 fill="none"
68 height={size}
69 stroke="currentColor"
70 strokeLinecap="round"
71 strokeLinejoin="round"
72 strokeWidth="2"
73 viewBox="0 0 24 24"
74 width={size}
75 xmlns="http://www.w3.org/2000/svg"
76 >
77 <motion.path
78 animate={controls}
79 d="m7 15 5 5 5-5"
80 transition={DEFAULT_TRANSITION}
81 variants={{
82 normal: { y: 0 },
83 animate: { y: [0, 2, 0] },
84 }}
85 />
86 <motion.path
87 animate={controls}
88 d="m7 9 5-5 5 5"
89 transition={DEFAULT_TRANSITION}
90 variants={{
91 normal: { y: 0 },
92 animate: { y: [0, -2, 0] },
93 }}
94 />
95 </svg>
96 </div>
97 );
98 },
99);
100
101ChevronsUpDownIcon.displayName = 'ChevronsUpDownIcon';
102
103export { ChevronsUpDownIcon };