167 lines · 4.8 KB · TypeScript
RawDownload
"use client"; import { useEffect, useId, useState, type RefObject } from "react";import { motion } from "motion/react"; import { cn } from "../../lib/utils"; export interface AnimatedBeamProps {  className?: string;  containerRef: RefObject<HTMLElement | null>; // Container ref  fromRef: RefObject<HTMLElement | null>;  toRef: RefObject<HTMLElement | null>;  curvature?: number;  reverse?: boolean;  pathColor?: string;  pathWidth?: number;  pathOpacity?: number;  gradientStartColor?: string;  gradientStopColor?: string;  delay?: number;  duration?: number;  repeat?: number;  repeatDelay?: number;  startXOffset?: number;  startYOffset?: number;  endXOffset?: number;  endYOffset?: number;} export const AnimatedBeam: React.FC<AnimatedBeamProps> = ({  className,  containerRef,  fromRef,  toRef,  curvature = 0,  reverse = false, // Include the reverse prop  duration = 5,  delay = 0,  pathColor = "gray",  pathWidth = 2,  pathOpacity = 0.2,  gradientStartColor = "#111111",  gradientStopColor = "#111111",  repeat = Infinity,  repeatDelay = 0,  startXOffset = 0,  startYOffset = 0,  endXOffset = 0,  endYOffset = 0,}) => {  const id = useId();  const [pathD, setPathD] = useState("");  const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 });   // Calculate the gradient coordinates based on the reverse prop  const gradientCoordinates = reverse    ? {        x1: ["90%", "-10%"],        x2: ["100%", "0%"],        y1: ["0%", "0%"],        y2: ["0%", "0%"],      }    : {        x1: ["10%", "110%"],        x2: ["0%", "100%"],        y1: ["0%", "0%"],        y2: ["0%", "0%"],      };   useEffect(() => {    const updatePath = () => {      if (containerRef.current && fromRef.current && toRef.current) {        const containerRect = containerRef.current.getBoundingClientRect();        const rectA = fromRef.current.getBoundingClientRect();        const rectB = toRef.current.getBoundingClientRect();         const svgWidth = containerRect.width;        const svgHeight = containerRect.height;        setSvgDimensions({ width: svgWidth, height: svgHeight });         const startX = rectA.left - containerRect.left + rectA.width / 2 + startXOffset;        const startY = rectA.top - containerRect.top + rectA.height / 2 + startYOffset;        const endX = rectB.left - containerRect.left + rectB.width / 2 + endXOffset;        const endY = rectB.top - containerRect.top + rectB.height / 2 + endYOffset;         const controlY = startY - curvature;        const d = `M ${startX},${startY} Q ${(startX + endX) / 2},${controlY} ${endX},${endY}`;        setPathD(d);      }    };     // Initialize ResizeObserver    const resizeObserver = new ResizeObserver(() => {      updatePath();    });     // Observe the container element    if (containerRef.current) {      resizeObserver.observe(containerRef.current);    }     // Call the updatePath initially to set the initial path    updatePath();     // Clean up the observer on component unmount    return () => {      resizeObserver.disconnect();    };  }, [containerRef, fromRef, toRef, curvature, startXOffset, startYOffset, endXOffset, endYOffset]);   return (    <svg      fill="none"      width={svgDimensions.width}      height={svgDimensions.height}      xmlns="http://www.w3.org/2000/svg"      className={cn("pointer-events-none absolute top-0 left-0 transform-gpu stroke-2", className)}      viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}    >      <path        d={pathD}        stroke={pathColor}        strokeWidth={pathWidth}        strokeOpacity={pathOpacity}        strokeLinecap="round"      />      <path        d={pathD}        strokeWidth={pathWidth}        stroke={`url(#${id})`}        strokeOpacity="1"        strokeLinecap="round"      />      <defs>        <motion.linearGradient          className="transform-gpu"          id={id}          gradientUnits={"userSpaceOnUse"}          initial={{            x1: "0%",            x2: "0%",            y1: "0%",            y2: "0%",          }}          animate={{            x1: gradientCoordinates.x1,            x2: gradientCoordinates.x2,            y1: gradientCoordinates.y1,            y2: gradientCoordinates.y2,          }}          transition={{            delay,            duration,            ease: [0.16, 1, 0.3, 1], // https://easings.net/#easeOutExpo            repeat,            repeatDelay,          }}        >          <stop stopColor={gradientStartColor} stopOpacity="0"></stop>          <stop stopColor={gradientStartColor}></stop>          <stop offset="32.5%" stopColor={gradientStopColor}></stop>          <stop offset="100%" stopColor={gradientStopColor} stopOpacity="0"></stop>        </motion.linearGradient>      </defs>    </svg>  );};