168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { motion, useTransform, type MotionValue } from "motion/react";
|
|
import { Pin } from "@/components/parallax/Pin";
|
|
import { Parallax } from "@/components/parallax/Parallax";
|
|
import { TRANSITIONS } from "@/lib/animation";
|
|
|
|
export function WorkSection() {
|
|
return (
|
|
<section id="work" aria-label="Selected Work" className="relative w-full">
|
|
{/* Subtle ambient background */}
|
|
<Parallax speed={0.04} className="pointer-events-none absolute inset-0 -z-10">
|
|
<div className="absolute inset-0 bg-[radial-gradient(1200px_600px_at_10%_20%,rgba(255,255,255,0.06),transparent_70%)]" />
|
|
</Parallax>
|
|
|
|
<Pin heightVH={400} className="w-full">
|
|
{(progress) => <WorkPinnedContent progress={progress} />}
|
|
</Pin>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function WorkPinnedContent({ progress }: { progress: MotionValue<number> }) {
|
|
// Safe to use React hooks here (top-level of a component)
|
|
const x = useTransform(progress, [0, 1], ["0%", "-400%"]);
|
|
|
|
return (
|
|
<div className="relative h-full w-full overflow-hidden">
|
|
{/* Section header overlays the sticky area */}
|
|
<div className="pointer-events-none absolute left-1/2 top-10 z-20 -translate-x-1/2 text-center">
|
|
<motion.h2
|
|
className="text-balance text-2xl font-semibold tracking-tight text-neutral-100 sm:text-3xl md:text-4xl"
|
|
initial={{ opacity: 0, y: 10 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={TRANSITIONS.base}
|
|
>
|
|
Highlights
|
|
</motion.h2>
|
|
<motion.p
|
|
className="mt-2 text-sm text-neutral-400"
|
|
initial={{ opacity: 0, y: 6 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ ...TRANSITIONS.base, delay: 0.05 }}
|
|
>
|
|
A scroll-pinned horizontal showcase powered by Lenis + Framer Motion.
|
|
</motion.p>
|
|
</div>
|
|
|
|
{/* Horizontal track */}
|
|
<motion.div
|
|
style={{ x }}
|
|
className="absolute inset-0 flex h-full w-[500vw] items-center gap-[5vw] px-[10vw]"
|
|
>
|
|
<WorkCard
|
|
title="Cinematic Portfolio"
|
|
subtitle="Parallax-first hero and narrative scroll"
|
|
/>
|
|
<WorkCard
|
|
title="Interactive Showcase"
|
|
subtitle="Pinned scenes and composited layers"
|
|
/>
|
|
<WorkCard
|
|
title="Motion System"
|
|
subtitle="Variants, micro-interactions, and rhythm"
|
|
/>
|
|
<WorkCard
|
|
title="A11y + Performance"
|
|
subtitle="Prefers-reduced-motion, optimized images"
|
|
/>
|
|
<WorkCard
|
|
title="Design Polish"
|
|
subtitle="Grain, gradients, light bloom and depth"
|
|
/>
|
|
</motion.div>
|
|
|
|
{/* Edge gradient masks for an infinite feel */}
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 w-24 bg-gradient-to-r from-black to-transparent" />
|
|
<div className="pointer-events-none absolute inset-y-0 right-0 w-24 bg-gradient-to-l from-black to-transparent" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkCard({
|
|
title,
|
|
subtitle,
|
|
}: {
|
|
title: string;
|
|
subtitle: string;
|
|
}) {
|
|
return (
|
|
<motion.article
|
|
className="group relative h-[66vh] w-[80vw] max-w-[720px] overflow-hidden rounded-3xl glass-strong p-6"
|
|
initial={{ opacity: 0, y: 24, scale: 0.98 }}
|
|
whileInView={{ opacity: 1, y: 0, scale: 1 }}
|
|
viewport={{ once: true, margin: "0px 0px -10% 0px" }}
|
|
transition={TRANSITIONS.base}
|
|
whileHover={{ scale: 1.02 }}
|
|
>
|
|
{/* Accent background layers with parallax depth */}
|
|
<Parallax speed={0.08} className="pointer-events-none absolute -inset-20 -z-10">
|
|
<div className="absolute inset-0 bg-[radial-gradient(600px_300px_at_70%_30%,rgba(255,255,255,0.08),transparent_70%)] blur-2xl" />
|
|
</Parallax>
|
|
<Parallax speed={-0.06} className="pointer-events-none absolute inset-0 -z-10">
|
|
<div className="absolute inset-0 bg-[radial-gradient(600px_300px_at_70%_30%,rgba(255,255,255,0.08),transparent_70%)]" />
|
|
</Parallax>
|
|
|
|
{/* Content */}
|
|
<div className="flex h-full flex-col justify-between">
|
|
<div>
|
|
<motion.h3
|
|
className="text-xl font-semibold tracking-tight text-neutral-100"
|
|
initial={{ opacity: 0, y: 8 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={TRANSITIONS.base}
|
|
>
|
|
{title}
|
|
</motion.h3>
|
|
<motion.p
|
|
className="mt-2 max-w-[48ch] text-sm text-neutral-300"
|
|
initial={{ opacity: 0, y: 6 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ ...TRANSITIONS.base, delay: 0.05 }}
|
|
>
|
|
{subtitle}
|
|
</motion.p>
|
|
</div>
|
|
|
|
{/* Placeholder visual block; replace with Next/Image for real work later */}
|
|
<motion.div
|
|
className="relative mt-6 flex flex-1 items-center justify-center overflow-hidden rounded-2xl glass-strong"
|
|
initial={{ opacity: 0, y: 10 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ ...TRANSITIONS.base, delay: 0.08 }}
|
|
>
|
|
<div className="absolute inset-0 bg-[linear-gradient(180deg,transparent,rgba(255,255,255,0.06)_40%,transparent_80%)]" />
|
|
<div className="text-xs text-neutral-400">Project visual placeholder</div>
|
|
</motion.div>
|
|
|
|
{/* Footer */}
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<motion.span
|
|
className="text-xs text-neutral-400"
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={TRANSITIONS.base}
|
|
>
|
|
Scroll to explore →
|
|
</motion.span>
|
|
<motion.button
|
|
className="rounded-full glass px-3 py-1 text-xs font-medium transition-colors hover:opacity-95"
|
|
whileHover={{ scale: 1.03 }}
|
|
whileTap={{ scale: 0.97 }}
|
|
>
|
|
View case
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</motion.article>
|
|
);
|
|
}
|