One approch to fix this is to use one more dashed line with same background as of page background.
Please refer to following code (its same as of you just some changes):
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import gsap from "gsap";
interface BoxItem {
label: string;
color: string;
}
interface Line {
x1: number;
y1: number;
x2: number;
y2: number;
}
const boxData: BoxItem[] = [
{ label: "Event", color: "#ff6b6b" },
{ label: "Date", color: "#4dabf7" },
{ label: "Fuel", color: "#f06595" },
{ label: "Message", color: "#51cf66" },
{ label: "Work", color: "#d0bfff" },
{ label: "Data", color: "#74c0fc" },
{ label: "Food", color: "#ffd43b" },
{ label: "Style", color: "#ced4da" },
];
export default function Home() {
const containerRef = useRef<HTMLDivElement | null>(null);
const centerBoxRef = useRef<HTMLDivElement | null>(null);
const boxRefs = useRef<(HTMLDivElement | null)[]>([]);
const [lines, setLines] = useState<Line[]>([]);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [activeLines, setActiveLines] = useState<Record<number, boolean>>({});
const timeoutRefs = useRef<any>({});
const animatingLines = useRef<Set<number>>(new Set());
// Track container size
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight,
});
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, []);
// Calculate line positions
useLayoutEffect(() => {
const updateLines = () => {
if (!centerBoxRef.current || !containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const centerRect = centerBoxRef.current.getBoundingClientRect();
const centerX = centerRect.left - containerRect.left + centerRect.width / 2;
const centerY = centerRect.top - containerRect.top + centerRect.height / 2;
const newLines: Line[] = boxRefs.current.map((box) => {
if (!box) return { x1: 0, y1: 0, x2: 0, y2: 0 };
const boxRect = box.getBoundingClientRect();
const x2 = boxRect.left - containerRect.left + boxRect.width / 2;
const y2 = boxRect.top - containerRect.top + boxRect.height / 2;
return { x1: centerX, y1: centerY, x2, y2 };
});
setLines(newLines);
};
updateLines();
const observer = new ResizeObserver(updateLines);
if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect();
}, [dimensions]);
const calculateCurvePath = (line: Line) => {
const cpX = (line.x1 + line.x2) / 2 + (line.y2 - line.y1) * -0.21;
const cpY = (line.y1 + line.y2) / 2 - (line.x2 - line.x1) * -0.21;
return {
path: `M${line.x1},${line.y1} Q${cpX},${cpY} ${line.x2},${line.y2}`,
};
};
const animateLine = (index: number, color: string) => {
const path = document.getElementById(`animated-line-${index}`) as SVGPathElement | null;
if (!path || animatingLines.current.has(index)) return;
animatingLines.current.add(index);
const length = path.getTotalLength();
// ✅ Key fix: make one full-length dash to reveal progressively
path.style.strokeDasharray = `${length}`;
path.style.strokeDashoffset = `${length}`;
path.style.stroke = color;
path.style.opacity = "1";
gsap.to(path, {
strokeDashoffset: 0,
duration: 0.8,
ease: "power1.inOut",
onComplete: () => {
setActiveLines((prev) => ({ ...prev, [index]: true }));
timeoutRefs.current[index] = setTimeout(() => reverseLine(index), 2000);
},
});
};
const reverseLine = (index: number) => {
const path = document.getElementById(`animated-line-${index}`) as SVGPathElement | null;
if (!path) return;
const length = path.getTotalLength();
gsap.to(path, {
strokeDashoffset: length,
duration: 0.6,
ease: "power2.inOut",
onComplete: () => {
path.style.opacity = "0";
animatingLines.current.delete(index);
setActiveLines((prev) => ({ ...prev, [index]: false }));
},
});
};
const handleBoxClick = (index: number, color: string) => {
if (animatingLines.current.has(index)) return;
if (timeoutRefs.current[index]) {
clearTimeout(timeoutRefs.current[index]);
}
if (!activeLines[index]) {
animateLine(index, color);
}
};
const handleCenterClick = () => {
boxData.forEach((box, i) => {
if (!activeLines[i] && !animatingLines.current.has(i)) {
animateLine(i, box.color);
}
});
};
return (
<div
ref={containerRef}
className="relative w-full h-screen bg-gradient-to-br from-pink-100 to-blue-100 overflow-hidden"
>
<svg className="absolute top-0 left-0 w-full h-full pointer-events-none">
<defs>
<linearGradient id="line-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#cccccc" />
<stop offset="100%" stopColor="#cccccc" stopOpacity="0.8" />
</linearGradient>
{/* New: background-matching gradient */}
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#ffe3ec" /> {/* matches from-pink-100 */}
<stop offset="100%" stopColor="#d0ebff" /> {/* matches to-blue-100 */}
</linearGradient>
</defs>
{lines.map((line, i) => {
const { path } = calculateCurvePath(line);
return (
<g key={i}>
{/* Static gray dashed line */}
<path
id={`dashed-line-${i}`}
d={path}
stroke="url(#line-gradient)"
strokeWidth="2"
strokeDasharray="8, 4"
fill="none"
/>
{/* Animated colored dashed overlay */}
<path
id={`animated-line-${i}`}
d={path}
stroke="transparent"
strokeWidth="2"
strokeDasharray="8, 4"
fill="none"
style={{ opacity: 0 }}
/>
{/* static white or background gap line */}
<path
id={`dashed-line-${i}`}
d={path}
stroke="url(#bg-gradient)"
strokeWidth="2"
strokeDasharray="8, 8"
fill="none"
/>
{/* Endpoint circle */}
<circle cx={line.x2} cy={line.y2} r="6" fill={boxData[i].color} />
</g>
);
})}
</svg>
{/* Center Circle */}
<div
ref={centerBoxRef}
onClick={(e) => {
e.stopPropagation();
handleCenterClick();
}}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 md:w-24 md:h-24 bg-white rounded-full shadow-lg grid place-items-center font-bold text-lg md:text-xl cursor-pointer z-10"
>
Any
</div>
{/* Outer Boxes */}
{boxData.map((box, i) => {
const angle = (360 / boxData.length) * i;
const radius = Math.min(dimensions.width, dimensions.height) * 0.35;
const rad = (angle * Math.PI) / 180;
const centerX = dimensions.width / 2;
const centerY = dimensions.height / 2;
const x = centerX + radius * Math.cos(rad);
const y = centerY + radius * Math.sin(rad);
return (
<div
key={i}
ref={(el) => (boxRefs.current[i] = el)}
onClick={(e) => {
e.stopPropagation();
handleBoxClick(i, box.color);
}}
className="absolute w-14 h-14 md:w-20 md:h-20 rounded-full shadow grid place-items-center text-xs md:text-sm font-bold cursor-pointer text-white"
style={{
backgroundColor: box.color,
left: `${x}px`,
top: `${y}px`,
transform: "translate(-50%, -50%)",
}}
>
{box.label}
</div>
);
})}
</div>
);
}