feat: add responsive carousel and dark theme to agents gallery section
This commit is contained in:
		
							
								
								
									
										28
									
								
								src/components/FadeIn.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/components/FadeIn.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { motion, type Transition } from 'framer-motion'
 | 
				
			||||||
 | 
					import React from 'react'
 | 
				
			||||||
 | 
					import { useMediaQuery } from '@/hooks/useMediaQuery'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FadeInProps = {
 | 
				
			||||||
 | 
					  children: React.ReactNode
 | 
				
			||||||
 | 
					  transition?: Transition
 | 
				
			||||||
 | 
					  className?: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function FadeIn({ children, transition, className }: FadeInProps) {
 | 
				
			||||||
 | 
					  const isMobile = useMediaQuery('(max-width: 768px)')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <motion.div
 | 
				
			||||||
 | 
					      className={className}
 | 
				
			||||||
 | 
					      initial={{ opacity: 0, y: 20 }}
 | 
				
			||||||
 | 
					      whileInView={{ opacity: 1, y: 0 }}
 | 
				
			||||||
 | 
					      viewport={{ once: false, margin: isMobile ? '0px 0px -50px 0px' : '0px 0px -100px 0px' }}
 | 
				
			||||||
 | 
					      transition={transition || { duration: 0.5 }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </motion.div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/hooks/useMediaQuery.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/hooks/useMediaQuery.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useEffect } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useMediaQuery(query: string) {
 | 
				
			||||||
 | 
					  const [matches, setMatches] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const media = window.matchMedia(query)
 | 
				
			||||||
 | 
					    if (media.matches !== matches) {
 | 
				
			||||||
 | 
					      setMatches(media.matches)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const listener = () => {
 | 
				
			||||||
 | 
					      setMatches(media.matches)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    media.addEventListener('change', listener)
 | 
				
			||||||
 | 
					    return () => media.removeEventListener('change', listener)
 | 
				
			||||||
 | 
					  }, [matches, query])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return matches
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								src/hooks/useResponsiveCarousel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/hooks/useResponsiveCarousel.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 🔧 Carousel Config
 | 
				
			||||||
 | 
					const desktopConfig = {
 | 
				
			||||||
 | 
					  GAP: 300,
 | 
				
			||||||
 | 
					  ROT_Y: 18,
 | 
				
			||||||
 | 
					  DEPTH: 210,
 | 
				
			||||||
 | 
					  SCALE_DROP: 0.12,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mobileConfig = {
 | 
				
			||||||
 | 
					  GAP: 110, // Smaller gap for mobile
 | 
				
			||||||
 | 
					  ROT_Y: 0,   // Flatter view on mobile
 | 
				
			||||||
 | 
					  DEPTH: 150, // Less depth
 | 
				
			||||||
 | 
					  SCALE_DROP: 0.1, // Less aggressive scaling
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useResponsiveCarousel = () => {
 | 
				
			||||||
 | 
					  const [config, setConfig] = useState(desktopConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const checkScreenSize = () => {
 | 
				
			||||||
 | 
					      if (window.innerWidth < 768) {
 | 
				
			||||||
 | 
					        setConfig(mobileConfig);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        setConfig(desktopConfig);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    checkScreenSize();
 | 
				
			||||||
 | 
					    window.addEventListener('resize', checkScreenSize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => window.removeEventListener('resize', checkScreenSize);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return config;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/hooks/useScroll.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/hooks/useScroll.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useEffect, useCallback } from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useScroll() {
 | 
				
			||||||
 | 
					  const [isAtBottom, setIsAtBottom] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleScroll = useCallback(() => {
 | 
				
			||||||
 | 
					    const footer = document.querySelector('footer')
 | 
				
			||||||
 | 
					    if (footer) {
 | 
				
			||||||
 | 
					      const footerTop = footer.getBoundingClientRect().top
 | 
				
			||||||
 | 
					      setIsAtBottom(footerTop < window.innerHeight)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    window.addEventListener('scroll', handleScroll)
 | 
				
			||||||
 | 
					    handleScroll() // Initial check
 | 
				
			||||||
 | 
					    return () => window.removeEventListener('scroll', handleScroll)
 | 
				
			||||||
 | 
					  }, [handleScroll])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const scrollToNext = () => {
 | 
				
			||||||
 | 
					    const sections = Array.from(
 | 
				
			||||||
 | 
					      document.querySelectorAll('section[id]')
 | 
				
			||||||
 | 
					    ) as HTMLElement[]
 | 
				
			||||||
 | 
					    const scrollPosition = window.scrollY + window.innerHeight / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentSection = sections.reduce((acc, section) => {
 | 
				
			||||||
 | 
					      return section.offsetTop < scrollPosition ? section : acc
 | 
				
			||||||
 | 
					    }, sections[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentIndex = sections.findIndex((sec) => sec.id === currentSection.id)
 | 
				
			||||||
 | 
					    const nextIndex = currentIndex + 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (nextIndex < sections.length) {
 | 
				
			||||||
 | 
					      sections[nextIndex].scrollIntoView({ behavior: 'smooth' })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const scrollToTop = () => {
 | 
				
			||||||
 | 
					    window.scrollTo({ top: 0, behavior: 'smooth' })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return { isAtBottom, scrollToNext, scrollToTop }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -36,7 +36,7 @@ const items = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function BentoSection() {
 | 
					export function BentoSection() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <section className="bg-white py-20 lg:py-32">
 | 
					    <section className="bg-black py-20 lg:py-32">
 | 
				
			||||||
      <Container>
 | 
					      <Container>
 | 
				
			||||||
        <motion.div
 | 
					        <motion.div
 | 
				
			||||||
          initial={{ opacity: 0, y: 20 }}
 | 
					          initial={{ opacity: 0, y: 20 }}
 | 
				
			||||||
@@ -45,10 +45,10 @@ export function BentoSection() {
 | 
				
			|||||||
          transition={{ duration: 0.8 }}
 | 
					          transition={{ duration: 0.8 }}
 | 
				
			||||||
          className="mx-auto max-w-3xl text-center mb-16"
 | 
					          className="mx-auto max-w-3xl text-center mb-16"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900">
 | 
					          <h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-50">
 | 
				
			||||||
            Augmented Intelligence Fabric
 | 
					            Augmented Intelligence Fabric
 | 
				
			||||||
          </h2>
 | 
					          </h2>
 | 
				
			||||||
          <p className="mt-6 text-lg text-gray-600">
 | 
					          <p className="mt-6 text-lg text-gray-400">
 | 
				
			||||||
            A complete infrastructure for building and deploying AI agents with enterprise-grade security and performance.
 | 
					            A complete infrastructure for building and deploying AI agents with enterprise-grade security and performance.
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        </motion.div>
 | 
					        </motion.div>
 | 
				
			||||||
@@ -61,11 +61,11 @@ export function BentoSection() {
 | 
				
			|||||||
              whileInView={{ opacity: 1, y: 0 }}
 | 
					              whileInView={{ opacity: 1, y: 0 }}
 | 
				
			||||||
              viewport={{ once: true }}
 | 
					              viewport={{ once: true }}
 | 
				
			||||||
              transition={{ duration: 0.5, delay: index * 0.1 }}
 | 
					              transition={{ duration: 0.5, delay: index * 0.1 }}
 | 
				
			||||||
              className="rounded-2xl bg-gray-50 border border-gray-200 p-6 hover:border-cyan-500 hover:shadow-lg transition-all duration-300"
 | 
					              className="rounded-2xl bg-gray-900 border border-gray-800 p-6 hover:border-cyan-500 hover:shadow-lg transition-all duration-300 hover:scale-105"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <h3 className="text-xl font-semibold text-gray-900">{item.title}</h3>
 | 
					              <h3 className="text-xl font-semibold text-gray-50">{item.title}</h3>
 | 
				
			||||||
              <p className="mt-2 text-sm font-medium text-cyan-500">{item.subtitle}</p>
 | 
					              <p className="mt-2 text-sm font-medium text-cyan-500">{item.subtitle}</p>
 | 
				
			||||||
              <p className="mt-3 text-sm text-gray-600">{item.description}</p>
 | 
					              <p className="mt-3 text-sm text-gray-400">{item.description}</p>
 | 
				
			||||||
            </motion.div>
 | 
					            </motion.div>
 | 
				
			||||||
          ))}
 | 
					          ))}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,58 +1,178 @@
 | 
				
			|||||||
import { motion } from 'framer-motion'
 | 
					'use client'
 | 
				
			||||||
import { Container } from '../../components/Container'
 | 
					
 | 
				
			||||||
 | 
					import { useEffect, useMemo, useState } from 'react'
 | 
				
			||||||
 | 
					import { useResponsiveCarousel } from '@/hooks/useResponsiveCarousel';
 | 
				
			||||||
 | 
					import { motion, AnimatePresence } from 'framer-motion'
 | 
				
			||||||
 | 
					import { wrap } from 'popmotion'
 | 
				
			||||||
 | 
					import { Button } from '@/components/Button';
 | 
				
			||||||
 | 
					import { SectionHeader, P, Eyebrow } from '@/components/Texts';
 | 
				
			||||||
 | 
					import { TypeAnimation } from 'react-type-animation'
 | 
				
			||||||
 | 
					import { FadeIn } from '@/components/FadeIn';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const galleryItems = [
 | 
					const galleryItems = [
 | 
				
			||||||
  { text: 'Navigate and interact with any web interface', image: '/images/gallery/interface.jpg' },
 | 
					  { text: 'Navigate and interact with any web interface', image: '/images/gallery/interface.jpg', width: 448, height: 277 },
 | 
				
			||||||
  { text: 'Process documents across all formats', image: '/images/gallery/docs.jpg' },
 | 
					  { text: 'Process documents across all formats', image: '/images/gallery/docs.jpg', width: 448, height: 277 },
 | 
				
			||||||
  { text: 'Execute multi-step workflows autonomously', image: '/images/gallery/flow.jpg' },
 | 
					  { text: 'Execute multi-step workflows autonomously', image: '/images/gallery/flow.jpg', width: 448, height: 277 },
 | 
				
			||||||
  { text: 'Manage calendars, emails, and tasks', image: '/images/gallery/calendar.jpg' },
 | 
					  { text: 'Manage calendars, emails, and tasks', image: '/images/gallery/calendar.jpg', width: 448, height: 277 },
 | 
				
			||||||
  { text: 'Perform deep semantic search across all data sources', image: '/images/gallery/data.jpg' },
 | 
					  { text: 'Perform deep semantic search across all data sources', image: '/images/gallery/data.jpg', width: 448, height: 277 },
 | 
				
			||||||
  { text: 'Identify patterns in complex datasets', image: '/images/gallery/datasets.jpg' },
 | 
					  { text: 'Identify patterns in complex datasets', image: '/images/gallery/datasets.jpg', width: 448, height: 277 },
 | 
				
			||||||
 | 
					  { text: 'Provide real-time market intelligence', image: '/images/gallery/market.jpg', width: 448, height: 277 },
 | 
				
			||||||
 | 
					  { text: 'Generate and debug code in multiple languages', image: '/images/gallery/code.jpg', width: 448, height: 277 },
 | 
				
			||||||
 | 
					  { text: 'Create consistent branded content', image: '/images/gallery/branding.jpg', width: 448, height: 277 },
 | 
				
			||||||
 | 
					  { text: 'Translate and localize materials', image: '/images/gallery/translate.jpg', width: 448, height: 277 },
 | 
				
			||||||
 | 
					  { text: 'Transform and migrate data structures', image: '/images/gallery/structure.jpg', width: 448, height: 277 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function GallerySection() {
 | 
					// 🔧 Carousel Config
 | 
				
			||||||
  return (
 | 
					const VISIBLE = 4;
 | 
				
			||||||
    <section className="bg-gray-50 py-20 lg:py-32">
 | 
					const AUTOPLAY_MS = 3200;
 | 
				
			||||||
      <Container>
 | 
					 | 
				
			||||||
        <motion.div
 | 
					 | 
				
			||||||
          initial={{ opacity: 0, y: 20 }}
 | 
					 | 
				
			||||||
          whileInView={{ opacity: 1, y: 0 }}
 | 
					 | 
				
			||||||
          viewport={{ once: true }}
 | 
					 | 
				
			||||||
          transition={{ duration: 0.8 }}
 | 
					 | 
				
			||||||
          className="mx-auto max-w-3xl text-center mb-16"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <h2 className="text-3xl lg:text-4xl font-medium tracking-tight text-gray-900">
 | 
					 | 
				
			||||||
            Agents with Endless Possibilities.
 | 
					 | 
				
			||||||
          </h2>
 | 
					 | 
				
			||||||
          <p className="mt-6 text-lg text-gray-600">
 | 
					 | 
				
			||||||
            Your private agent coordinates a team of specialists that spin up on demand, collaborate across your world, and deliver end-to-end results. Many agents, one intelligence—yours.
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </motion.div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
 | 
					export function GallerySection() {
 | 
				
			||||||
          {galleryItems.map((item, index) => (
 | 
					  const [active, setActive] = useState(0);
 | 
				
			||||||
            <motion.div
 | 
					  const [hovering, setHovering] = useState(false);
 | 
				
			||||||
              key={index}
 | 
					  const { GAP, ROT_Y, DEPTH, SCALE_DROP } = useResponsiveCarousel();
 | 
				
			||||||
              initial={{ opacity: 0, y: 20 }}
 | 
					
 | 
				
			||||||
              whileInView={{ opacity: 1, y: 0 }}
 | 
					  // autoplay
 | 
				
			||||||
              viewport={{ once: true }}
 | 
					  useEffect(() => {
 | 
				
			||||||
              transition={{ duration: 0.5, delay: index * 0.1 }}
 | 
					    if (hovering) return
 | 
				
			||||||
              className="group relative overflow-hidden rounded-2xl bg-white border border-gray-200 hover:border-cyan-500 hover:shadow-lg transition-all duration-300"
 | 
					    const id = setInterval(() => setActive((i) => wrap(0, galleryItems.length, i + 1)), AUTOPLAY_MS)
 | 
				
			||||||
            >
 | 
					    return () => clearInterval(id)
 | 
				
			||||||
              <div className="aspect-video overflow-hidden">
 | 
					  }, [hovering])
 | 
				
			||||||
                <img
 | 
					
 | 
				
			||||||
                  src={item.image}
 | 
					  const indices = useMemo(
 | 
				
			||||||
                  alt={item.text}
 | 
					    () => [...Array(VISIBLE * 2 + 1)].map((_, i) => wrap(0, galleryItems.length, active + i - VISIBLE)),
 | 
				
			||||||
                  className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
 | 
					    [active]
 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
              <div className="p-6">
 | 
					 | 
				
			||||||
                <p className="text-sm font-medium text-gray-900">{item.text}</p>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </motion.div>
 | 
					 | 
				
			||||||
          ))}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </Container>
 | 
					 | 
				
			||||||
    </section>
 | 
					 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const next = () => setActive((i) => wrap(0, galleryItems.length, i + 1))
 | 
				
			||||||
 | 
					  const prev = () => setActive((i) => wrap(0, galleryItems.length, i - 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="bg-[#FAFAFA]">
 | 
				
			||||||
 | 
					      <div className="relative isolate pt-8 pb-0 text-center w-full">
 | 
				
			||||||
 | 
					        <FadeIn transition={{ duration: 0.8, delay: 0.1 }}>
 | 
				
			||||||
 | 
					          <div className="mx-auto max-w-5xl lg:mt-12">
 | 
				
			||||||
 | 
					            <Eyebrow color="accent">Use Cases</Eyebrow>
 | 
				
			||||||
 | 
					            <SectionHeader className="text-center" color="dark">Agents with Endless Possibilities.</SectionHeader>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </FadeIn>
 | 
				
			||||||
 | 
					        <FadeIn transition={{ duration: 0.8, delay: 0.2 }}>
 | 
				
			||||||
 | 
					          <div className="mx-auto max-w-4xl mt-6 lg:px-0 px-4">
 | 
				
			||||||
 | 
					            <P className="text-center" color="dark">
 | 
				
			||||||
 | 
					            Your private agent coordinates a team of specialists that spin up on demand, collaborate across your world, and deliver end-to-end results.
 | 
				
			||||||
 | 
					            Many agents, one intelligence—yours.
 | 
				
			||||||
 | 
					            </P>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </FadeIn>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <FadeIn transition={{ duration: 1, delay: 0.4 }}>
 | 
				
			||||||
 | 
					        <section
 | 
				
			||||||
 | 
					          className="relative w-full flex items-center justify-center overflow-hidden -mt-8 pt-0 pb-0"
 | 
				
			||||||
 | 
					          onMouseEnter={() => setHovering(true)}
 | 
				
			||||||
 | 
					          onMouseLeave={() => setHovering(false)}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					                    <div className="relative w-full max-w-[1800px] h-[300px] md:h-[500px]" style={{ perspective: '1600px' }}>
 | 
				
			||||||
 | 
					            <div className="absolute inset-0" style={{ transformStyle: 'preserve-3d' }}>
 | 
				
			||||||
 | 
					              <AnimatePresence initial={false}>
 | 
				
			||||||
 | 
					                {indices.map((idx, i) => {
 | 
				
			||||||
 | 
					                  const distance = i - VISIBLE;
 | 
				
			||||||
 | 
					                  const item = galleryItems[idx];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  const x = distance * GAP;
 | 
				
			||||||
 | 
					                  const z = -Math.abs(distance) * DEPTH;
 | 
				
			||||||
 | 
					                  const r = distance * ROT_Y;
 | 
				
			||||||
 | 
					                  const s = 1 - Math.abs(distance) * SCALE_DROP;
 | 
				
			||||||
 | 
					                  const o = distance === 0 ? 1 : 0.80;
 | 
				
			||||||
 | 
					                  const zIndex = 100 - Math.abs(distance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  return (
 | 
				
			||||||
 | 
					                    <motion.div
 | 
				
			||||||
 | 
					                      key={`${idx}-${i}`}
 | 
				
			||||||
 | 
					                      className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 will-change-transform overflow-hidden ${distance === 0 ? 'rounded-xl' : ''}`}
 | 
				
			||||||
 | 
					                      initial={{ opacity: 0 }}
 | 
				
			||||||
 | 
					                      animate={{
 | 
				
			||||||
 | 
					                        transform: `translateX(${x}px) translateZ(${z}px) rotateY(${r}deg) scale(${s})`,
 | 
				
			||||||
 | 
					                        zIndex,
 | 
				
			||||||
 | 
					                        opacity: o,
 | 
				
			||||||
 | 
					                        boxShadow: distance === 0 ? '0 0 20px 5px rgba(0, 0, 0, 0.1)' : 'none',
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                      exit={{ opacity: 0 }}
 | 
				
			||||||
 | 
					                      transition={{ type: 'spring', stiffness: 220, damping: 26 }}
 | 
				
			||||||
 | 
					                      onClick={() => setActive(idx)}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <div className="relative bg-gray-100 flex items-center justify-center">
 | 
				
			||||||
 | 
					                        <img
 | 
				
			||||||
 | 
					                          src={item.image}
 | 
				
			||||||
 | 
					                          alt={item.text}
 | 
				
			||||||
 | 
					                          width={item.width}
 | 
				
			||||||
 | 
					                          height={item.height}
 | 
				
			||||||
 | 
					                          className="object-contain"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </motion.div>
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					              </AnimatePresence>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {/* Arrows */}
 | 
				
			||||||
 | 
					          <div className="absolute inset-y-0 left-8 hidden md:flex items-center z-50">
 | 
				
			||||||
 | 
					            <button
 | 
				
			||||||
 | 
					              onClick={prev}
 | 
				
			||||||
 | 
					              className="bg-white/50 rounded-full p-2 shadow-lg backdrop-blur-md text-black"
 | 
				
			||||||
 | 
					              aria-label="Previous"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <svg className="size-8" viewBox="0 0 24 24" fill="none" dangerouslySetInnerHTML={{ __html: '<path d="M15 19L8 12l7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>' }} />
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="absolute inset-y-0 right-8 hidden md:flex items-center z-50">
 | 
				
			||||||
 | 
					            <button
 | 
				
			||||||
 | 
					              onClick={next}
 | 
				
			||||||
 | 
					              className="bg-white/50 rounded-full p-2 shadow-lg backdrop-blur-md text-black"
 | 
				
			||||||
 | 
					              aria-label="Next"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <svg className="size-8" viewBox="0 0 24 24" fill="none" dangerouslySetInnerHTML={{ __html: '<path d="M9 5l7 7-7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>' }} />
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {/* Foreground pill (Desktop) */}
 | 
				
			||||||
 | 
					          <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[60] hidden md:block">
 | 
				
			||||||
 | 
					            <div className="flex items-center justify-between w-[1040px] gap-6 rounded-2xl bg-gray-100/80 shadow-[0_8px_40px_rgba(0,0,0,0.15)] px-12 backdrop-blur">
 | 
				
			||||||
 | 
					              <P as="h4" className="max-w-[820px] h-[72px] flex items-center" color="dark">
 | 
				
			||||||
 | 
					                <TypeAnimation
 | 
				
			||||||
 | 
					                  key={active}
 | 
				
			||||||
 | 
					                  sequence={[galleryItems[active].text]}
 | 
				
			||||||
 | 
					                  wrapper="span"
 | 
				
			||||||
 | 
					                  speed={50}
 | 
				
			||||||
 | 
					                  repeat={0}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </P>
 | 
				
			||||||
 | 
					              <Button href="#" color="cyan" className="text-sm px-4 py-2 lg:text-base whitespace-nowrap">
 | 
				
			||||||
 | 
					                Start
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* Text box (Mobile) */}
 | 
				
			||||||
 | 
					        <div className="md:hidden w-full px-4 -mt-12 mb-16">
 | 
				
			||||||
 | 
					            <div className="flex flex-row items-center justify-between w-full gap-x-4 rounded-2xl bg-gray-100/80 p-4 backdrop-blur-md">
 | 
				
			||||||
 | 
					              <P as="h4" className="w-full text-left h-[72px] leading-tight flex items-center" color="dark">
 | 
				
			||||||
 | 
					                <TypeAnimation
 | 
				
			||||||
 | 
					                  key={active}
 | 
				
			||||||
 | 
					                  sequence={[galleryItems[active].text]}
 | 
				
			||||||
 | 
					                  wrapper="span"
 | 
				
			||||||
 | 
					                  speed={50}
 | 
				
			||||||
 | 
					                  repeat={0}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </P>
 | 
				
			||||||
 | 
					              <Button href="#" color="cyan" className="text-xs px-3 py-1.5 whitespace-nowrap">
 | 
				
			||||||
 | 
					                 Start
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </FadeIn>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user