diff --git a/app/components/CaseCard.tsx b/app/components/CaseCard.tsx index 98470b0..8fa6869 100644 --- a/app/components/CaseCard.tsx +++ b/app/components/CaseCard.tsx @@ -3,13 +3,17 @@ /** * CASE CARD — replaces the rejected repetitive photos with a coded, animated * data visual unique per case: - * - 3D tilt that tracks the pointer (Motion springs, transform-only). - * - An animated bars/sparkline "result chart" drawn in CSS + SVG, growing - * into view (no two cards look alike: different bars, colours, labels). + * - 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. - * Reduced-motion / touch: flat card, bars still grow on view via CSS. + * - 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 { useRef } from "react"; +import { useEffect, useRef } from "react"; import { motion, useMotionValue, @@ -17,6 +21,8 @@ import { useTransform, useReducedMotion, } from "motion/react"; +import { gsap } from "./gsap"; +import { SPRING, EASE_OUT } from "./motion"; export type CaseData = { tag: string; @@ -29,17 +35,23 @@ export type CaseData = { accent: string; // brand accent for this card }; -export default function CaseCard({ data, index }: { data: CaseData; index: number }) { +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], [7, -7]), { stiffness: 200, damping: 20 }); - const ry = useSpring(useTransform(mx, [0, 1], [-9, 9]), { stiffness: 200, damping: 20 }); + 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.14), transparent 45%)` + `radial-gradient(circle at ${gx * 100}% ${gy * 100}%, rgba(255,255,255,0.16), transparent 45%)` ); const onMove = (e: React.PointerEvent) => { @@ -55,26 +67,57 @@ export default function CaseCard({ data, index }: { data: CaseData; index: numbe 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 */} -