"use client"; import React, { useState, useRef, useLayoutEffect } from "react"; import { gsap } from "gsap"; // Define the props for the reusable component. interface SparkleNavbarProps { /** * An array of strings representing the navigation menu items. * Each string will be the text for a button. * @example ['Home', 'About', 'Contact'] */ items: string[]; /** * The color for the active state text shadow, box shadow, and other effects. * @example '#1E90FF' (a shade of blue) */ color?: string; } /** * A reusable navigation menu component with a dynamic, animated active state indicator. * All CSS and animation logic are self-contained within this single TSX file. * * @param {SparkleNavbarProps} props - The component props. * @returns {JSX.Element} The rendered navigation menu. */ const SparkleNavbar: React.FC = ({ items, color = "#00fffc", }) => { const [activeIndex, setActiveIndex] = useState(0); // Refs to get direct access to DOM elements for animations. const navRef = useRef(null); const activeElementRef = useRef(null); const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); // Function to create the SVG content for the active element. const createSVG = (element: HTMLDivElement) => { element.innerHTML = `
`; }; // Helper function to calculate the horizontal offset for the active element. const getOffsetLeft = (element: HTMLButtonElement) => { if (!navRef.current || !activeElementRef.current) return 0; const elementRect = element.getBoundingClientRect(); const navRect = navRef.current.getBoundingClientRect(); const activeElementWidth = activeElementRef.current.offsetWidth; return ( elementRect.left - navRect.left + (elementRect.width - activeElementWidth) / 2 ); }; // useLayoutEffect runs synchronously after all DOM mutations, ensuring the // initial position of the active element is correct before the first paint. useLayoutEffect(() => { const activeButton = buttonRefs.current[activeIndex]; if (navRef.current && activeElementRef.current && activeButton) { gsap.set(activeElementRef.current, { x: getOffsetLeft(activeButton), }); gsap.to(activeElementRef.current, { "--active-element-show": "1", duration: 0.2, }); } }, []); // Handler for button clicks, which triggers the animation. const handleClick = (index: number) => { const navElement = navRef.current; const activeElement = activeElementRef.current; const oldButton = buttonRefs.current[activeIndex]; const newButton = buttonRefs.current[index]; if ( index === activeIndex || !navElement || !activeElement || !oldButton || !newButton ) { return; } const x = getOffsetLeft(newButton); const direction = index > activeIndex ? "after" : "before"; const spacing = Math.abs(x - getOffsetLeft(oldButton)); navElement.classList.add(direction); gsap.set(activeElement, { rotateY: direction === "before" ? "180deg" : "0deg", }); gsap.to(activeElement, { keyframes: [ { "--active-element-width": `${spacing > navElement.offsetWidth - 60 ? navElement.offsetWidth - 60 : spacing}px`, duration: 0.3, ease: "none", onStart: () => { createSVG(activeElement); gsap.to(activeElement, { "--active-element-opacity": 1, duration: 0.1, }); }, }, { "--active-element-scale-x": "0", "--active-element-scale-y": ".25", "--active-element-width": "0px", duration: 0.3, onStart: () => { gsap.to(activeElement, { "--active-element-mask-position": "40%", duration: 0.5, }); gsap.to(activeElement, { "--active-element-opacity": 0, delay: 0.45, duration: 0.25, }); }, onComplete: () => { activeElement.innerHTML = ""; navElement.classList.remove("before", "after"); gsap.set(activeElement, { x: getOffsetLeft(newButton), "--active-element-show": "1", }); // Update the state after the animation completes to trigger a re-render // with the new active item. setActiveIndex(index); }, }, ], }); gsap.to(activeElement, { x, "--active-element-strike-x": "-50%", duration: 0.6, ease: "none", }); }; return ( <> {/* The main container for the component, replicating the body styles. */} ); }; export default SparkleNavbar;