add
This commit is contained in:
142
src/components/ClickableGallery.tsx
Normal file
142
src/components/ClickableGallery.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { wrap } from 'popmotion'
|
||||
import { Button } from '@/components/Button'
|
||||
|
||||
const galleryItems = [
|
||||
{ text: 'Navigate and interact with any web interface', image: '/images/interface.png' },
|
||||
{ text: 'Process documents across all formats', image: '/images/docs.png' },
|
||||
{ text: 'Execute multi-step workflows autonomously', image: '/images/flow.png' },
|
||||
{ text: 'Manage calendars, emails, and tasks', image: '/images/calendar.png' },
|
||||
{ text: 'Perform deep semantic search across all data sources', image: '/images/data.png' },
|
||||
{ text: 'Identify patterns in complex datasets', image: '/images/datasets.png' },
|
||||
{ text: 'Provide real-time market intelligence', image: '/images/market.png' },
|
||||
{ text: 'Generate and debug code in multiple languages', image: '/images/code.png' },
|
||||
{ text: 'Create consistent branded content', image: '/images/branding.png' },
|
||||
{ text: 'Translate and localize materials', image: '/images/translate.png' },
|
||||
{ text: 'Transform and migrate data structures', image: '/images/structure.png' },
|
||||
]
|
||||
|
||||
// 🔧 Carousel Config
|
||||
const VISIBLE = 4
|
||||
const CARD_SIZE = 450 // square size on desktop
|
||||
const GAP = 380 // spacing for larger cards
|
||||
const ROT_Y = 18
|
||||
const DEPTH = 260
|
||||
const SCALE_DROP = 0.12
|
||||
const AUTOPLAY_MS = 3200
|
||||
|
||||
export function ClickableGallery() {
|
||||
const [active, setActive] = useState(0)
|
||||
const [hovering, setHovering] = useState(false)
|
||||
|
||||
// autoplay
|
||||
useEffect(() => {
|
||||
if (hovering) return
|
||||
const id = setInterval(() => setActive((i) => wrap(0, galleryItems.length, i + 1)), AUTOPLAY_MS)
|
||||
return () => clearInterval(id)
|
||||
}, [hovering])
|
||||
|
||||
const indices = useMemo(
|
||||
() => [...Array(VISIBLE * 2 + 1)].map((_, i) => wrap(0, galleryItems.length, active + i - VISIBLE)),
|
||||
[active]
|
||||
)
|
||||
|
||||
const next = () => setActive((i) => wrap(0, galleryItems.length, i + 1))
|
||||
const prev = () => setActive((i) => wrap(0, galleryItems.length, i - 1))
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative w-full h-[92vh] flex items-center justify-center overflow-hidden bg-background"
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
{/* Soft edge fades */}
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 w-32 bg-gradient-to-r from-background to-transparent" />
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-background to-transparent" />
|
||||
|
||||
<div className="relative w-full max-w-[1800px] h-[900px]" 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.85
|
||||
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"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
transform: `translateX(${x}px) translateZ(${z}px) rotateY(${r}deg) scale(${s})`,
|
||||
zIndex,
|
||||
opacity: o,
|
||||
}}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 220, damping: 26 }}
|
||||
onClick={() => setActive(idx)}
|
||||
>
|
||||
{/* Square container, keeps image ratio inside */}
|
||||
<div
|
||||
className="relative rounded-2xl overflow-hidden bg-white flex items-center justify-center"
|
||||
style={{ width: CARD_SIZE, height: CARD_SIZE }}
|
||||
>
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.text}
|
||||
fill
|
||||
className="object-contain rounded-2xl"
|
||||
priority={i === VISIBLE}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Arrows */}
|
||||
<div className="absolute inset-y-0 left-8 flex items-center z-50">
|
||||
<button
|
||||
onClick={prev}
|
||||
className="bg-white/70 hover:bg-white rounded-full p-3 shadow-lg backdrop-blur-md"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<svg className="size-8" viewBox="0 0 24 24" fill="none"><path d="M15 19L8 12l7-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-8 flex items-center z-50">
|
||||
<button
|
||||
onClick={next}
|
||||
className="bg-white/70 hover:bg-white rounded-full p-3 shadow-lg backdrop-blur-md"
|
||||
aria-label="Next"
|
||||
>
|
||||
<svg className="size-8" viewBox="0 0 24 24" fill="none"><path d="M9 5l7 7-7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Foreground pill */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[60]">
|
||||
<div className="flex items-center gap-6 rounded-3xl bg-white/95 shadow-[0_8px_40px_rgba(0,0,0,0.15)] px-12 py-6 backdrop-blur">
|
||||
<h2 className="text-2xl lg:text-3xl font-semibold leading-tight text-black max-w-[820px]">
|
||||
{galleryItems[active].text}
|
||||
</h2>
|
||||
<Button href="#" color="cyan" className="text-sm px-4 py-2 lg:text-base">
|
||||
Start
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
@@ -516,7 +516,11 @@ function FeaturesMobile() {
|
||||
{features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
ref={(ref) => ref && (slideRefs.current[featureIndex] = ref)}
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
slideRefs.current[featureIndex] = ref
|
||||
}
|
||||
}}
|
||||
className="w-full flex-none snap-center px-4 sm:px-6"
|
||||
>
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-gray-800 px-5 py-6">
|
||||
|
Reference in New Issue
Block a user