"use client"; /** * CASE CARD — replaces the rejected repetitive photos with a coded, animated * data visual unique per case: * - 3D tilt that tracks the pointer via a SOFT spring (momentum => the card * floats and settles, never snaps). Transform-only. * - A small lift + accent shadow on hover; a press dip (scale 0.985) so the * whole card shares the page's tactile press language. * - An animated bars + sparkline "result chart" (CSS + GSAP DrawSVG on the * spark) growing into view — no two cards look alike. * - A glare/sheen that follows the cursor across the surface. * - The chart panel gets a clip-path inset() wipe on first view (premium). * Reduced-motion / touch: flat card, bars still grow on view via CSS. */ import { useEffect, useRef } from "react"; import { motion, useMotionValue, useSpring, useTransform, useReducedMotion, } from "motion/react"; import { gsap } from "./gsap"; import { SPRING, EASE_OUT } from "./motion"; export type CaseData = { tag: string; problem: string; result: string; how: string; metricNum: string; metricLabel: string; bars: number[]; // 0..100 heights — unique per case accent: string; // brand accent for this card }; export default function CaseCard({ data, index, }: { data: CaseData; index: number; }) { const reduce = useReducedMotion(); const ref = useRef(null); const mx = useMotionValue(0.5); const my = useMotionValue(0.5); const rx = useSpring(useTransform(my, [0, 1], [6, -6]), SPRING.tilt); const ry = useSpring(useTransform(mx, [0, 1], [-8, 8]), SPRING.tilt); const glare = useTransform( [mx, my], ([gx, gy]: number[]) => `radial-gradient(circle at ${gx * 100}% ${gy * 100}%, rgba(255,255,255,0.16), transparent 45%)` ); const onMove = (e: React.PointerEvent) => { if (reduce || e.pointerType !== "mouse") return; const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); mx.set((e.clientX - r.left) / r.width); my.set((e.clientY - r.top) / r.height); }; const reset = () => { mx.set(0.5); my.set(0.5); }; // DrawSVG the sparkline once the card scrolls in — a small, deliberate detail. useEffect(() => { const el = ref.current; if (!el || reduce) return; const ctx = gsap.context(() => { gsap.from(el.querySelector(".case__spark polyline"), { drawSVG: "0%", duration: 1.1, ease: "power2.out", scrollTrigger: { trigger: el, start: "top 82%", once: true }, delay: 0.15 + index * 0.05, }); }, el); return () => ctx.revert(); }, [reduce, index]); return (
{String(index + 1).padStart(2, "0")} {data.tag}
{/* coded data visual — unique bars per case, clip-wiped on first view */}

Before {data.problem}

{data.result}

{data.how}

{data.metricNum} {data.metricLabel}
{!reduce && (
); }