| import { useState, useEffect, useCallback, useMemo } from 'react'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
| import { FiMenu, FiX, FiExternalLink, FiArrowUpRight } from 'react-icons/fi'; |
| import { HiSparkles } from 'react-icons/hi'; |
|
|
| export default function Navbar() { |
| const [isOpen, setIsOpen] = useState(false); |
| const [scrolled, setScrolled] = useState(false); |
|
|
| |
| const menuItems = useMemo( |
| () => [ |
| { label: 'Features', href: '#features' }, |
| { label: 'Converters', href: '#converters' }, |
| { label: 'Social Media', href: '#social-media' }, |
| { label: 'About', href: '#about' }, |
| ], |
| [], |
| ); |
|
|
| |
| const handleScroll = useCallback(() => { |
| const scrollTop = window.scrollY; |
| setScrolled(scrollTop > 20); |
| }, []); |
|
|
| useEffect(() => { |
| let ticking = false; |
|
|
| const throttledScroll = () => { |
| if (!ticking) { |
| requestAnimationFrame(() => { |
| handleScroll(); |
| ticking = false; |
| }); |
| ticking = true; |
| } |
| }; |
|
|
| window.addEventListener('scroll', throttledScroll, { passive: true }); |
| return () => window.removeEventListener('scroll', throttledScroll); |
| }, [handleScroll]); |
|
|
| |
| useEffect(() => { |
| const handleClickOutside = event => { |
| if (isOpen && !event.target.closest('nav')) { |
| setIsOpen(false); |
| } |
| }; |
|
|
| document.addEventListener('click', handleClickOutside); |
| return () => document.removeEventListener('click', handleClickOutside); |
| }, [isOpen]); |
|
|
| |
| const easeInOutCubic = (t) => { |
| return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; |
| }; |
|
|
| const smoothScrollTo = useCallback((targetPosition, duration = 1200) => { |
| const startPosition = window.pageYOffset; |
| const distance = targetPosition - startPosition; |
| let startTime = null; |
|
|
| const animation = (currentTime) => { |
| if (startTime === null) startTime = currentTime; |
| const timeElapsed = currentTime - startTime; |
| const progress = Math.min(timeElapsed / duration, 1); |
| const ease = easeInOutCubic(progress); |
| |
| window.scrollTo(0, startPosition + distance * ease); |
| |
| if (timeElapsed < duration) { |
| requestAnimationFrame(animation); |
| } |
| }; |
|
|
| requestAnimationFrame(animation); |
| }, []); |
|
|
| |
| const handleSmoothScroll = useCallback( |
| href => { |
| if (href.startsWith('#')) { |
| const element = document.querySelector(href); |
| if (element) { |
| const offset = 80; |
| const elementPosition = element.getBoundingClientRect().top; |
| const offsetPosition = elementPosition + window.pageYOffset - offset; |
|
|
| smoothScrollTo(offsetPosition, 1200); |
| setIsOpen(false); |
| } |
| } |
| }, |
| [smoothScrollTo, setIsOpen], |
| ); |
|
|
| |
| const scrollToTop = useCallback(() => { |
| smoothScrollTo(0, 1000); |
| }, [smoothScrollTo]); |
|
|
| return ( |
| <motion.nav |
| className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ease-out ${ |
| scrolled |
| ? 'bg-[var(--background-secondary)]/95 backdrop-blur-xl border-b border-[var(--border)] shadow-lg' |
| : 'bg-transparent' |
| }`} |
| initial={{ y: -100, opacity: 0 }} |
| animate={{ y: 0, opacity: 1 }} |
| transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }} |
| > |
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div className="flex items-center justify-between h-14 sm:h-16"> |
| {/* Logo */} |
| <motion.div |
| className="flex items-center space-x-2 cursor-pointer group" |
| whileHover={{ scale: 1.02 }} |
| whileTap={{ scale: 0.98 }} |
| transition={{ duration: 0.2 }} |
| onClick={scrollToTop} |
| > |
| <div className="relative"> |
| <HiSparkles className="text-xl sm:text-2xl text-[var(--accent)] group-hover:text-[var(--accent-hover)] transition-colors duration-300" /> |
| <div className="absolute inset-0 bg-[var(--accent)] blur-lg opacity-20 group-hover:opacity-30 transition-opacity duration-300"></div> |
| </div> |
| <span className="text-lg sm:text-xl font-bold gradient-text"> |
| LumaKit |
| </span> |
| </motion.div> |
| |
| {/* Desktop Menu */} |
| <div className="hidden md:flex items-center space-x-6 lg:space-x-8"> |
| {menuItems.map((item, index) => ( |
| <motion.button |
| key={index} |
| onClick={() => handleSmoothScroll(item.href)} |
| className="text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 text-sm font-medium relative group" |
| whileHover={{ y: -1 }} |
| transition={{ duration: 0.2 }} |
| > |
| {item.label} |
| <span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[var(--accent)] group-hover:w-full transition-all duration-300"></span> |
| </motion.button> |
| ))} |
| <motion.a |
| onClick={() => handleSmoothScroll("#features")} |
| rel="noopener noreferrer" |
| className="group relative flex items-center space-x-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-[var(--background)] px-4 py-2 rounded-xl transition-all duration-300 text-sm font-medium shadow-lg hover:shadow-xl overflow-hidden" |
| whileHover={{ scale: 1.02, y: -1 }} |
| whileTap={{ scale: 0.98 }} |
| transition={{ duration: 0.2 }} |
| > |
| <div className="absolute inset-0 bg-gradient-to-r from-[var(--accent)] to-[var(--accent-hover)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> |
| <FiExternalLink className="text-sm relative z-10" /> |
| <span className="relative z-10">Try LumaKit</span> |
| </motion.a> |
| </div> |
| |
| {/* Mobile Menu Button */} |
| <motion.button |
| className="md:hidden text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 p-2 rounded-lg hover:bg-[var(--background-secondary)]" |
| onClick={() => setIsOpen(!isOpen)} |
| whileTap={{ scale: 0.95 }} |
| aria-label="Toggle menu" |
| > |
| <motion.div |
| animate={isOpen ? 'open' : 'closed'} |
| variants={{ |
| open: { rotate: 180 }, |
| closed: { rotate: 0 }, |
| }} |
| transition={{ duration: 0.3 }} |
| > |
| {isOpen ? <FiX size={20} /> : <FiMenu size={20} />} |
| </motion.div> |
| </motion.button> |
| </div> |
| |
| {/* Mobile Menu */} |
| <AnimatePresence mode="wait"> |
| {isOpen && ( |
| <motion.div |
| className="md:hidden absolute top-full left-0 right-0 bg-[var(--background-secondary)]/98 backdrop-blur-xl border-b border-[var(--border)] shadow-2xl" |
| initial={{ opacity: 0, height: 0, y: -10 }} |
| animate={{ opacity: 1, height: 'auto', y: 0 }} |
| exit={{ opacity: 0, height: 0, y: -10 }} |
| transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }} |
| > |
| <div className="px-4 py-6 space-y-4"> |
| {menuItems.map((item, index) => ( |
| <motion.button |
| key={index} |
| onClick={() => handleSmoothScroll(item.href)} |
| className="block w-full text-left text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors duration-300 text-base font-medium py-2 px-3 rounded-lg hover:bg-[var(--background-tertiary)]" |
| initial={{ opacity: 0, x: -20 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ delay: index * 0.1, duration: 0.3 }} |
| > |
| {item.label} |
| </motion.button> |
| ))} |
| <motion.a |
| href="https://huggingface.co/spaces/YoruAkio/LumaKit" |
| target="_blank" |
| rel="noopener noreferrer" |
| className="group relative flex items-center space-x-2 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-[var(--background)] px-4 py-3 rounded-xl transition-all duration-300 text-base font-medium w-fit shadow-lg overflow-hidden" |
| onClick={() => setIsOpen(false)} |
| initial={{ opacity: 0, x: -20 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ delay: 0.4, duration: 0.3 }} |
| > |
| <div className="absolute inset-0 bg-gradient-to-r from-[var(--accent)] to-[var(--accent-hover)] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> |
| <FiExternalLink className="text-sm relative z-10" /> |
| <span className="relative z-10">Try LumaKit</span> |
| <FiArrowUpRight className="text-sm group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform duration-200 relative z-10" /> |
| </motion.a> |
| </div> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </div> |
| </motion.nav> |
| ); |
| } |