waypoints.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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 WaypointsIconHandle {
  8. startAnimation: () => void;
  9. stopAnimation: () => void;
  10. }
  11. interface WaypointsIconProps extends HTMLAttributes<HTMLDivElement> {
  12. size?: number;
  13. }
  14. const VARIANTS: Variants = {
  15. normal: {
  16. pathLength: 1,
  17. opacity: 1,
  18. },
  19. animate: (custom: number) => ({
  20. pathLength: [0, 1],
  21. opacity: [0, 1],
  22. transition: {
  23. delay: 0.15 * custom,
  24. opacity: { delay: 0.1 * custom },
  25. },
  26. }),
  27. };
  28. const WaypointsIcon = forwardRef<WaypointsIconHandle, WaypointsIconProps>(
  29. ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
  30. const controls = useAnimation();
  31. const isControlledRef = useRef(false);
  32. useImperativeHandle(ref, () => {
  33. isControlledRef.current = true;
  34. return {
  35. startAnimation: () => controls.start("animate"),
  36. stopAnimation: () => controls.start("normal"),
  37. };
  38. });
  39. const handleMouseEnter = useCallback(
  40. (e: React.MouseEvent<HTMLDivElement>) => {
  41. if (isControlledRef.current) {
  42. onMouseEnter?.(e);
  43. } else {
  44. controls.start("animate");
  45. }
  46. },
  47. [controls, onMouseEnter]
  48. );
  49. const handleMouseLeave = useCallback(
  50. (e: React.MouseEvent<HTMLDivElement>) => {
  51. if (isControlledRef.current) {
  52. onMouseLeave?.(e);
  53. } else {
  54. controls.start("normal");
  55. }
  56. },
  57. [controls, onMouseLeave]
  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.circle
  78. animate={controls}
  79. custom={0}
  80. cx="12"
  81. cy="4.5"
  82. r="2.5"
  83. variants={VARIANTS}
  84. />
  85. <motion.path
  86. animate={controls}
  87. custom={1}
  88. d="m10.2 6.3-3.9 3.9"
  89. variants={VARIANTS}
  90. />
  91. <motion.circle
  92. animate={controls}
  93. custom={0}
  94. cx="4.5"
  95. cy="12"
  96. r="2.5"
  97. variants={VARIANTS}
  98. />
  99. <motion.path
  100. animate={controls}
  101. custom={2}
  102. d="M7 12h10"
  103. variants={VARIANTS}
  104. />
  105. <motion.circle
  106. animate={controls}
  107. custom={0}
  108. cx="19.5"
  109. cy="12"
  110. r="2.5"
  111. variants={VARIANTS}
  112. />
  113. <motion.path
  114. animate={controls}
  115. custom={3}
  116. d="m13.8 17.7 3.9-3.9"
  117. variants={VARIANTS}
  118. />
  119. <motion.circle
  120. animate={controls}
  121. custom={0}
  122. cx="12"
  123. cy="19.5"
  124. r="2.5"
  125. variants={VARIANTS}
  126. />
  127. </svg>
  128. </div>
  129. );
  130. }
  131. );
  132. WaypointsIcon.displayName = "WaypointsIcon";
  133. export { WaypointsIcon };