// Coverflow Gallery — Originkit // Originkit — defaults rewritten to match preview. "use client" import { useState, useEffect, useCallback, useRef, type CSSProperties, } from "react" const useIsStaticRenderer = () => false interface Slide { image?: { src?: string; srcSet?: string; alt?: string } title?: string } type AutoplayDir = "leftToRight" | "rightToLeft" type TitleCorner = "topLeft" | "topRight" | "bottomLeft" | "bottomRight" interface Smooth3DSlideshowProps { slides?: Slide[] cardWidth?: number cardHeight?: number radius?: number tilt?: number sideTilt?: number gap?: number opacity?: number transition?: any autoplay?: boolean autoplayDirection?: AutoplayDir showTitle?: boolean titleFont?: CSSProperties titleColor?: string titlePosition?: { position?: TitleCorner paddingLeft?: number paddingRight?: number paddingTop?: number paddingBottom?: number } style?: CSSProperties } const DEFAULT_SLIDES: Slide[] = [ { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/316d1761-fd79-4ca9-b8d4-f2bb20521a00/w=800", }, title: "For Sitting\nMetal\nMinimal", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/aeaa0756-9647-4f6c-d900-204bd25e4a00/w=800", }, title: "For Living\nConcrete\nForm", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/34ce1842-4b7a-4d52-0302-38582c341700/w=800", }, title: "For Working\nSteel\nClean", }, ] // Fixed internals (no longer exposed as controls). const PERSPECTIVE = 1600 const SCALE_STEP = 0.16 const MAX_VISIBLE = 2 // In a preserve-3d context paint order follows 3D position, not z-index, so the // centre is pushed nearest the viewer and neighbours fall back behind it. const DEPTH = 240 // Derive a CSS transition (duration + easing) from a Framer Transition value. function cssTransition(t: any): { dur: number; ease: string } { const dur = t && typeof t.duration === "number" ? t.duration : 0.6 let ease = "cubic-bezier(0.22, 1, 0.36, 1)" const e = t?.ease if (Array.isArray(e) && e.length === 4) { ease = `cubic-bezier(${e[0]}, ${e[1]}, ${e[2]}, ${e[3]})` } else if (typeof e === "string") { const map: Record = { linear: "linear", easeIn: "ease-in", easeOut: "ease-out", easeInOut: "ease-in-out", } ease = map[e] || "ease" } return { dur, ease } } /** * Smooth 3D Slideshow * * A 3D coverflow: the active card sits upright in the spotlight while its * neighbours tilt back in perspective. Click any card to smoothly bring it to * centre. Recreated after Tanya Prokofieva's Framer original. * * @framerIntrinsicWidth 900 * @framerIntrinsicHeight 560 * * @framerSupportedLayoutWidth any-prefer-fixed * @framerSupportedLayoutHeight any-prefer-fixed */ export default function Smooth3DSlideshow(props: Smooth3DSlideshowProps) { props = { ...COMPONENT_DEFAULTS, ...props } const { slides = DEFAULT_SLIDES, cardWidth = 557, cardHeight = 420, radius = 0, tilt = 7, sideTilt = 7, gap = 7, opacity = 65, transition, autoplay = false, autoplayDirection = "rightToLeft", showTitle = false, titleFont, titleColor = "#ffffff", titlePosition, style, } = props const tp = titlePosition || {} const corner: TitleCorner = tp.position || "bottomLeft" const isTop = corner === "topLeft" || corner === "topRight" const isRight = corner === "topRight" || corner === "bottomRight" const padLeft = tp.paddingLeft ?? 22 const padRight = tp.paddingRight ?? 22 const padTop = tp.paddingTop ?? 24 const padBottom = tp.paddingBottom ?? 24 const isStatic = useIsStaticRenderer() const list = slides && slides.length ? slides : DEFAULT_SLIDES const n = list.length // Loop is always on. const loop = true const [active, setActive] = useState(0) // Keep active valid if the slide list changes. useEffect(() => { setActive((a) => Math.max(0, Math.min(n - 1, a))) }, [n]) // Lock input while a card is mid-move; release once it settles, so rapid // clicks/keys don't stack up and look jittery. Duration comes from the // transition (default 0.6s). const moveDur = transition && typeof transition.duration === "number" ? transition.duration : 0.6 const lockRef = useRef(false) const lock = useCallback(() => { lockRef.current = true window.setTimeout( () => { lockRef.current = false }, Math.max(50, moveDur * 1000) ) }, [moveDur]) const step = useCallback( (dir: number) => { if (lockRef.current) return lock() setActive((a) => (((a + dir) % n) + n) % n) }, [n, lock] ) const handleCardClick = useCallback( (i: number) => { if (isStatic || autoplay || lockRef.current) return lock() setActive((a) => (i === a ? (a + 1) % n : i)) }, [isStatic, autoplay, n, lock] ) // Autoplay — the transition's Delay drives the time each card holds. const delay = transition && typeof transition.delay === "number" ? transition.delay : 2.5 useEffect(() => { if (isStatic || !autoplay || n < 2) return const ms = Math.max(0.3, delay) * 1000 const dir = autoplayDirection === "leftToRight" ? -1 : 1 const id = window.setInterval(() => step(dir), ms) return () => window.clearInterval(id) }, [isStatic, autoplay, autoplayDirection, delay, n, step]) const onKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "ArrowRight") { e.preventDefault() step(1) } else if (e.key === "ArrowLeft") { e.preventDefault() step(-1) } }, [step] ) const { dur, ease } = cssTransition(transition) const transitionCss = `transform ${dur}s ${ease}, opacity ${dur}s ${ease}` // Rounded scale 0–20: boxy at 0, fully rounded (pill on the short axis) at 20. const effectiveRadius = (Math.max(0, Math.min(20, radius)) / 20) * (Math.min(cardWidth, cardHeight) / 2) // Inactive opacity: 100% = fully visible, 0% = hidden. Overlay is the inverse. const dim = 1 - Math.max(0, Math.min(100, opacity)) / 100 const rootStyle: CSSProperties = { ...(style || {}), position: "relative", width: "100%", height: "100%", minWidth: 320, minHeight: 360, display: "flex", alignItems: "center", justifyContent: "center", perspective: `${PERSPECTIVE}px`, overflow: "hidden", outline: "none", } return (
{list.map((slide, i) => { let rel = i - active if (loop) { if (rel > n / 2) rel -= n if (rel < -n / 2) rel += n } const ax = Math.abs(rel) const visible = ax <= MAX_VISIBLE const isActive = rel === 0 const sc = Math.max(0.4, 1 - ax * SCALE_STEP) // Gap 0–20 → spacing 0 (stacked) to ~600px (far apart). const tx = rel * (gap * 30) const tz = -ax * DEPTH const ry = -rel * tilt const rz = rel * sideTilt const src = slide.image?.src || "" const cardStyle: CSSProperties = { position: "absolute", left: "50%", top: "50%", width: cardWidth, height: cardHeight, borderRadius: effectiveRadius, overflow: "hidden", transformStyle: "preserve-3d", transformOrigin: "center center", transform: `translate(-50%, -50%) translateX(${tx}px) translateZ(${tz}px) rotateY(${ry}deg) rotateZ(${rz}deg) scale(${sc})`, transition: transitionCss, opacity: visible ? 1 : 0, cursor: autoplay || isActive ? "default" : "pointer", pointerEvents: visible && !isStatic && !autoplay ? "auto" : "none", backgroundColor: "#1a1a1a", } return (
handleCardClick(i) } aria-label={slide.title} aria-hidden={!visible} > {src ? ( {slide.image?.alt ) : null} {showTitle && ( <> {/* Gradient for legibility (matches corner) */}
{/* Title at chosen corner */}
{slide.title}
)} {/* Dim overlay (darkens inactive cards entirely) */}
) })}
) } const COMPONENT_DEFAULTS = { slides: [ { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/7d4d2641-d6a8-4fef-e85c-b12ed100d500/w=800", }, title: "James Walker", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/933a7615-f4b6-4eae-8ed1-705fa0e24400/w=800", }, title: "Olivia Carter", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/859c75ea-953e-489e-be61-91a03a35d700/w=800", }, title: "Amelia Foster", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/31afae9c-5ba3-4ec3-2534-ed8198ed1100/w=800", }, title: "Benjamin Harris", }, { image: { src: "https://imagedelivery.net/IEUjvl3YUlxY-MrTpOAWDQ/ed7b1c40-3332-43d8-a9eb-4615ef341b00/w=800", }, title: "Lucas Martin", }, ], cardWidth: 400, cardHeight: 400, radius: 3, tilt: 12, sideTilt: 8, gap: 8, opacity: 60, autoplay: false, autoplayDirection: "rightToLeft", transition: { type: "tween", duration: 0.6, delay: 2.5, ease: [0.22, 1, 0.36, 1], }, showTitle: true, titleFont: { fontFamily: "Inter", variant: "Bold", fontSize: "28px", letterSpacing: "-0.02em", lineHeight: "1.1em", } as any, titleColor: "#ffffff", titlePosition: { position: "bottomLeft", paddingLeft: 22, paddingRight: 22, paddingTop: 24, paddingBottom: 24, }, }

Preparando Sorteo...