diff --git a/src/app/tuxedo/page.tsx b/src/app/tuxedo/page.tsx index 4c344cd..e37241a 100644 --- a/src/app/tuxedo/page.tsx +++ b/src/app/tuxedo/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import Link from "next/link"; -import { motion, useInView } from "framer-motion"; +import { motion, useInView, AnimatePresence } from "framer-motion"; import { useCart } from "@/context/CartContext"; import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero"; import LayoutContainer from "@/components/layout/LayoutContainer"; @@ -13,6 +13,13 @@ import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import BrandStylesProvider from "@/components/storefront/BrandStylesProvider"; import { supabase } from "@/lib/supabase"; import { getBrandSettingsPublic } from "@/actions/brand-settings"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Register GSAP plugins +if (typeof window !== "undefined") { + gsap.registerPlugin(ScrollTrigger); +} type Brand = { id: string; @@ -494,6 +501,112 @@ export default function TuxedoPage() { document.getElementById("products")?.scrollIntoView({ behavior: "smooth" }); } + // ─── GSAP SCROLL ANIMATIONS ────────────────────────────────────────────── + useEffect(() => { + if (typeof window === "undefined") return; + + const ctx = gsap.context(() => { + // Parallax floating elements + gsap.utils.toArray(".parallax-float").forEach((el, i) => { + gsap.to(el, { + y: -60 - i * 15, + scrollTrigger: { + trigger: el, + start: "top bottom", + end: "bottom top", + scrub: 1, + }, + }); + }); + + // Staggered text reveal on scroll + const revealTextElements = gsap.utils.toArray(".reveal-text"); + revealTextElements.forEach((el) => { + gsap.fromTo( + el, + { opacity: 0, y: 40, skewY: 2 }, + { + opacity: 1, + y: 0, + skewY: 0, + duration: 1, + ease: "power3.out", + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none reverse", + }, + } + ); + }); + + // Scale reveal for cards + const revealScaleElements = gsap.utils.toArray(".reveal-scale"); + revealScaleElements.forEach((el, i) => { + gsap.fromTo( + el, + { opacity: 0, scale: 0.9, y: 40 }, + { + opacity: 1, + scale: 1, + y: 0, + duration: 0.8, + delay: i * 0.08, + ease: "back.out(1.4)", + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none reverse", + }, + } + ); + }); + + // Counter animations + const counterElements = gsap.utils.toArray(".counter-animate"); + counterElements.forEach((el) => { + const target = parseInt(el.getAttribute("data-target") || "0", 10); + const obj = { val: 0 }; + gsap.to(obj, { + val: target, + duration: 2, + ease: "power2.out", + onUpdate: () => { + if (el.textContent !== null) { + el.textContent = Math.floor(obj.val).toLocaleString(); + } + }, + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none none", + }, + }); + }); + + // Horizontal reveal for section headers + gsap.utils.toArray(".reveal-slide").forEach((el) => { + gsap.fromTo( + el, + { opacity: 0, x: -40 }, + { + opacity: 1, + x: 0, + duration: 0.8, + ease: "power3.out", + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none reverse", + }, + } + ); + }); + }); + + return () => ctx.revert(); + }, []); + const featuredProducts = products.slice(0, 3); return ( @@ -547,12 +660,12 @@ export default function TuxedoPage() {
-

Delivery Stops

-

+

Delivery Stops

+

Upcoming
Stops

-
-

+

+

Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.

@@ -594,13 +707,13 @@ export default function TuxedoPage() {
-
+

Farm-Direct

-

- This Week's
Harvest +

+ This Week's
Harvest

-
-

+

+

Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.

@@ -638,38 +751,73 @@ export default function TuxedoPage() {
-
+
+ {/* Parallax decorative elements */} +
+
+
+
+ -
-

Our Story

-

- Three Generations of Sweet Corn Excellence +
+

Our Story

+

+ Three Generations of
Sweet Corn Excellence

-
-

- Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years. +

+

+ Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.

-
+ + {/* Stats with counter animation */} +
{[ - { stat: "40+", label: "Years" }, - { stat: "3", label: "Generations" }, - { stat: "100%", label: "Hand-Picked" }, - ].map(({ stat, label }) => ( -
-

{stat}

-

{label}

+ { stat: "40", suffix: "+", label: "Years Growing" }, + { stat: "3", suffix: "", label: "Generations" }, + { stat: "100", suffix: "%", label: "Hand-Picked" }, + ].map((item, i) => ( +
+
+ 0 + {item.suffix} +
+
+ {item.label} +
))}
- - Read Our Story - - - - + +
+ { + e.currentTarget.style.transform = "translateY(-3px)"; + e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = "translateY(0)"; + e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)"; + }} + > + Read Our Story + + + + +

diff --git a/src/components/storefront/TuxedoVideoHero.tsx b/src/components/storefront/TuxedoVideoHero.tsx index 302eb3c..f16f990 100644 --- a/src/components/storefront/TuxedoVideoHero.tsx +++ b/src/components/storefront/TuxedoVideoHero.tsx @@ -3,6 +3,13 @@ import Image from "next/image"; import { useEffect, useRef, useState } from "react"; import { motion } from "framer-motion"; +import { gsap } from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Register GSAP plugins +if (typeof window !== "undefined") { + gsap.registerPlugin(ScrollTrigger); +} type TuxedoVideoHeroProps = { eyebrow?: string; @@ -21,42 +28,9 @@ const VIDEO_URL = const OLATHE_SWEET_LOGO_DARK = "https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png"; -// Staggered animation variants -const fadeUpVariants = { - hidden: { opacity: 0, y: 24 }, - visible: (delay: number = 0) => ({ - opacity: 1, - y: 0, - transition: { - duration: 0.8, - ease: [0.25, 0.1, 0.25, 1] as [number, number, number, number], - delay, - }, - }), -}; - -const containerVariants = { - hidden: {}, - visible: { - transition: { - staggerChildren: 0.15, - delayChildren: 0.3, - }, - }, -}; - -// Scroll indicator animation -const bounceVariants = { - animate: { - y: [0, 8, 0], - transition: { - duration: 1.6, - repeat: Infinity, - ease: "easeInOut" as const, - }, - }, -}; - +// ───────────────────────────────────────────────────────────────────────────── +// CINEMATIC HERO - SCROLL-DRIVEN ANIMATIONS +// ───────────────────────────────────────────────────────────────────────────── export default function TuxedoVideoHero({ eyebrow, title, @@ -69,7 +43,10 @@ export default function TuxedoVideoHero({ }: TuxedoVideoHeroProps) { const logoSrc = olatheSweetLogoUrl ?? OLATHE_SWEET_LOGO_DARK; const sectionRef = useRef(null); + const contentRef = useRef(null); + const videoRef = useRef(null); const [isVisible, setIsVisible] = useState(false); + const [scrollProgress, setScrollProgress] = useState(0); // Trigger entrance animation on mount useEffect(() => { @@ -77,6 +54,103 @@ export default function TuxedoVideoHero({ return () => clearTimeout(timer); }, []); + // ─── GSAP SCROLL ANIMATIONS ──────────────────────────────────────────────── + useEffect(() => { + if (typeof window === "undefined" || !sectionRef.current) return; + + const ctx = gsap.context(() => { + // Scroll progress tracking + ScrollTrigger.create({ + trigger: sectionRef.current, + start: "top top", + end: "bottom top", + scrub: true, + onUpdate: (self) => { + setScrollProgress(self.progress); + }, + }); + + // Hero content pin - fade out on scroll + if (contentRef.current) { + gsap.to(contentRef.current, { + y: -80, + opacity: 0, + ease: "power2.inOut", + scrollTrigger: { + trigger: sectionRef.current, + start: "top top", + end: "40% top", + scrub: 1.2, + }, + }); + } + + // Video scale + parallax + if (videoRef.current) { + gsap.to(videoRef.current, { + scale: 1.15, + y: 100, + ease: "power2.inOut", + scrollTrigger: { + trigger: sectionRef.current, + start: "top top", + end: "50% top", + scrub: 1.5, + }, + }); + } + + // Staggered entrance for content elements + const contentElements = gsap.utils.toArray(".hero-reveal"); + contentElements.forEach((el, i) => { + gsap.fromTo( + el, + { opacity: 0, y: 40, scale: 0.95 }, + { + opacity: 1, + y: 0, + scale: 1, + duration: 1, + ease: "power3.out", + scrollTrigger: { + trigger: el, + start: "top 90%", + toggleActions: "play none none reverse", + }, + } + ); + }); + + // Parallax floating elements + const floatElements = gsap.utils.toArray(".parallax-float"); + floatElements.forEach((el, idx) => { + gsap.to(el, { + y: -80 - idx * 20, + scrollTrigger: { + trigger: el, + start: "top bottom", + end: "bottom top", + scrub: 1, + }, + }); + }); + + // Scroll indicator bounce on scroll + gsap.to(".scroll-indicator", { + y: 20, + opacity: 0, + scrollTrigger: { + trigger: sectionRef.current, + start: "10% top", + end: "30% top", + scrub: 1, + }, + }); + }, sectionRef); + + return () => ctx.revert(); + }, []); + // Smooth scroll to stops section const handlePrimaryClick = () => { const stopsSection = document.getElementById("stops"); @@ -88,160 +162,326 @@ export default function TuxedoVideoHero({ }; return ( -
- {/* Background video */} -
+
+ + {/* ─── CORNER DECORATIONS ───────────────────────────────────────────── */} +
+ + + + +
+
+ + + + + +
+ + + {/* ─── GLOBAL ANIMATIONS ──────────────────────────────────────────────── */} + + ); } \ No newline at end of file