activity.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. "use client";
  2. import type { Variants } from "motion/react";
  3. import { motion, useAnimation } from "motion/react";
  4. import type { HTMLAttributes } from "react";
  5. import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
  6. import { cn } from "@/lib/utils";
  7. export interface ActivityIconHandle {
  8. startAnimation: () => void;
  9. stopAnimation: () => void;
  10. }
  11. interface ActivityIconProps extends HTMLAttributes<HTMLDivElement> {
  12. size?: number;
  13. }
  14. const VARIANTS: Variants = {
  15. normal: {
  16. opacity: 1,
  17. pathLength: 1,
  18. pathOffset: 0,
  19. transition: {
  20. duration: 0.4,
  21. opacity: { duration: 0.1 },
  22. },
  23. },
  24. animate: {
  25. opacity: [0, 1],
  26. pathLength: [0, 1],
  27. pathOffset: [1, 0],
  28. transition: {
  29. duration: 0.6,
  30. ease: "linear",
  31. opacity: { duration: 0.1 },
  32. },
  33. },
  34. };
  35. const ActivityIcon = forwardRef<ActivityIconHandle, ActivityIconProps>(
  36. ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
  37. const controls = useAnimation();
  38. const isControlledRef = useRef(false);
  39. useImperativeHandle(ref, () => {
  40. isControlledRef.current = true;
  41. return {
  42. startAnimation: () => controls.start("animate"),
  43. stopAnimation: () => controls.start("normal"),
  44. };
  45. });
  46. const handleMouseEnter = useCallback(
  47. (e: React.MouseEvent<HTMLDivElement>) => {
  48. if (isControlledRef.current) {
  49. onMouseEnter?.(e);
  50. } else {
  51. controls.start("animate");
  52. }
  53. },
  54. [controls, onMouseEnter]
  55. );
  56. const handleMouseLeave = useCallback(
  57. (e: React.MouseEvent<HTMLDivElement>) => {
  58. if (isControlledRef.current) {
  59. onMouseLeave?.(e);
  60. } else {
  61. controls.start("normal");
  62. }
  63. },
  64. [controls, onMouseLeave]
  65. );
  66. return (
  67. <div
  68. className={cn(className)}
  69. onMouseEnter={handleMouseEnter}
  70. onMouseLeave={handleMouseLeave}
  71. {...props}
  72. >
  73. <svg
  74. fill="none"
  75. height={size}
  76. stroke="currentColor"
  77. strokeLinecap="round"
  78. strokeLinejoin="round"
  79. strokeWidth="2"
  80. viewBox="0 0 24 24"
  81. width={size}
  82. xmlns="http://www.w3.org/2000/svg"
  83. >
  84. <motion.path
  85. animate={controls}
  86. d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
  87. initial="normal"
  88. variants={VARIANTS}
  89. />
  90. </svg>
  91. </div>
  92. );
  93. }
  94. );
  95. ActivityIcon.displayName = "ActivityIcon";
  96. export { ActivityIcon };