Animated Beam
An animated beam of light traveling along a path, perfect for showcasing website integration features
An animated beam of light traveling along a path, perfect for showcasing website integration features
npm install framer-motion
1'use client';23import { cn } from '@/lib/utils';4import { motion } from 'framer-motion';5import { forwardRef, RefObject, useEffect, useId, useState } from 'react';67export interface AnimatedBeamProps {8className?: string;9containerRef: RefObject<HTMLElement>; // Container ref10fromRef: RefObject<HTMLElement>;11toRef: RefObject<HTMLElement>;12curvature?: number;13reverse?: boolean;14pathColor?: string;15pathWidth?: number;16pathOpacity?: number;17gradientStartColor?: string;18gradientStopColor?: string;19delay?: number;20duration?: number;21startXOffset?: number;22startYOffset?: number;23endXOffset?: number;24endYOffset?: number;25dotted?: boolean;26dotSpacing?: number;27}2829export const AnimatedBeam: React.FC<AnimatedBeamProps> = ({30className,31containerRef,32fromRef,33toRef,34curvature = 0,35reverse = false, // Include the reverse prop36duration = Math.random() * 3 + 4,37delay = 0,38pathColor = 'gray',39pathWidth = 2,40pathOpacity = 0.2,41gradientStartColor = '#4d40ff',42gradientStopColor = '#4043ff',43startXOffset = 0,44startYOffset = 0,45endXOffset = 0,46endYOffset = 0,47dotted = false,48dotSpacing = 6,49}) => {50const id = useId();51const [pathD, setPathD] = useState('');5253const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 });54const strokeDasharray = dotted ? `${dotSpacing} ${dotSpacing}` : 'none';55// Calculate the gradient coordinates based on the reverse prop56const gradientCoordinates = reverse57? {58x1: ['90%', '-10%'],59x2: ['100%', '0%'],60y1: ['0%', '0%'],61y2: ['0%', '0%'],62}63: {64x1: ['10%', '110%'],65x2: ['0%', '100%'],66y1: ['0%', '0%'],67y2: ['0%', '0%'],68};6970useEffect(() => {71const updatePath = () => {72if (containerRef.current && fromRef.current && toRef.current) {73const containerRect = containerRef.current.getBoundingClientRect();74const rectA = fromRef.current.getBoundingClientRect();75const rectB = toRef.current.getBoundingClientRect();7677const svgWidth = containerRect.width;78const svgHeight = containerRect.height;79setSvgDimensions({ width: svgWidth, height: svgHeight });8081const startX =82rectA.left - containerRect.left + rectA.width / 2 + startXOffset;83const startY =84rectA.top - containerRect.top + rectA.height / 2 + startYOffset;85const endX =86rectB.left - containerRect.left + rectB.width / 2 + endXOffset;87const endY =88rectB.top - containerRect.top + rectB.height / 2 + endYOffset;8990const controlY = startY - curvature;91const d = `M ${startX},${startY} Q ${92(startX + endX) / 293},${controlY} ${endX},${endY}`;94setPathD(d);95}96};9798// Initialize ResizeObserver99const resizeObserver = new ResizeObserver((entries) => {100// For all entries, recalculate the path101for (let entry of entries) {102updatePath();103}104});105106// Observe the container element107if (containerRef.current) {108resizeObserver.observe(containerRef.current);109}110111// Call the updatePath initially to set the initial path112updatePath();113114// Clean up the observer on component unmount115return () => {116resizeObserver.disconnect();117};118}, [119containerRef,120fromRef,121toRef,122curvature,123startXOffset,124startYOffset,125endXOffset,126endYOffset,127]);128129return (130<svg131fill='none'132width={svgDimensions.width}133height={svgDimensions.height}134xmlns='http://www.w3.org/2000/svg'135className={cn(136'pointer-events-none absolute left-0 top-0 transform-gpu stroke-2',137className138)}139viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}140>141<path142d={pathD}143stroke={pathColor}144strokeWidth={pathWidth}145strokeOpacity={pathOpacity}146strokeLinecap='round'147strokeDasharray={strokeDasharray}148/>149<motion.path150d={pathD}151stroke={`url(#${id})`}152strokeLinecap='round'153strokeDasharray={strokeDasharray}154initial={{155strokeWidth: pathWidth,156strokeOpacity: 0,157}}158animate={{159strokeWidth: pathWidth * 1.5, // or any scale factor you prefer160strokeOpacity: 1,161}}162transition={{163duration: 2, // adjust as needed164delay: delay, // use the same delay as the gradient animation165}}166/>167<defs>168<motion.linearGradient169className='transform-gpu'170id={id}171gradientUnits={'userSpaceOnUse'}172initial={{173x1: '0%',174x2: '0%',175y1: '0%',176y2: '0%',177}}178animate={{179x1: gradientCoordinates.x1,180x2: gradientCoordinates.x2,181y1: gradientCoordinates.y1,182y2: gradientCoordinates.y2,183}}184transition={{185delay,186duration,187ease: [0.16, 1, 0.3, 1], // https://easings.net/#easeOutExpo188repeat: Infinity,189repeatDelay: 0,190}}191>192<stop stopColor={gradientStartColor} stopOpacity='0'></stop>193<stop stopColor={gradientStartColor}></stop>194<stop offset='32.5%' stopColor={gradientStopColor}></stop>195<stop196offset='100%'197stopColor={gradientStopColor}198stopOpacity='0'199></stop>200</motion.linearGradient>201</defs>202</svg>203);204};205206export const Circle = forwardRef<207HTMLDivElement,208{ className?: string; children?: React.ReactNode }209>(({ className, children }, ref) => {210return (211<div212ref={ref}213className={cn(214'z-10 flex h-12 w-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]',215className216)}217>218{children}219</div>220);221});
Prop | Type | Description | Default |
---|---|---|---|
className | string | The class name for the SVG element. | - |
containerRef | RefObject | The ref for the container element. | - |
fromRef | RefObject | The ref of the element from which the beam should start. | - |
toRef | RefObject | The ref of the element to which the beam should end. | - |
curvature | number | The curvature of the beam. | 0 |
reverse | boolean | Whether the beam should be reversed. | false |
duration | number | The duration of the beam animation. | Random (4-7) |
delay | number | The delay before the beam animation starts. | 0 |
pathColor | string | The color of the beam path. | "gray" |
pathWidth | number | The width of the beam path. | 2 |
pathOpacity | number | The opacity of the beam path. | 0.2 |
gradientStartColor | string | The start color of the gradient for the beam. | "#4d40ff" |
gradientStopColor | string | The stop color of the gradient for the beam. | "#4043ff" |
startXOffset | number | The x offset of the beam's start position. | 0 |
startYOffset | number | The y offset of the beam's start position. | 0 |
endXOffset | number | The x offset of the beam's end position. | 0 |
endYOffset | number | The y offset of the beam's end position. | 0 |
dotted | boolean | Whether the beam should be dotted. | false |
dotSpacing | number | The spacing between dots if the beam is dotted. | 6 |