"use client";
import * as React from "react";
import Image from "next/image";
import { motion, useMotionValue, useSpring } from "framer-motion";
export type WorkItem = {
readonly name: string;
readonly href: string;
readonly client: string;
readonly text: string;
readonly did: readonly string[];
readonly tech: readonly string[];
readonly desktop: {
readonly src: string;
readonly width: number;
readonly height: number;
};
readonly mobile: {
readonly src: string;
readonly width: number;
readonly height: number;
};
};
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em]";
/** How far a tall screenshot has to move so its end reaches the bottom of the frame. */
const travel = (
shot: { width: number; height: number },
frameAspect: number,
): string => {
const visible = (shot.width / frameAspect / shot.height) * 100;
return `-${Math.max(0, 100 - visible).toFixed(2)}%`;
};
/** A screenshot that scrolls slowly through its frame while it is on screen. */
const ScrollingShot: React.FC<{
shot: { src: string; width: number; height: number };
frameAspect: number;
alt: string;
sizes: string;
active: boolean;
seconds: number;
}> = ({ shot, frameAspect, alt, sizes, active, seconds }) => (
);
const WorkStage: React.FC<{ item: WorkItem }> = ({ item }) => {
const ref = React.useRef(null);
const [inView, setInView] = React.useState(false);
const [still, setStill] = React.useState(false);
React.useEffect(() => {
setStill(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const node = ref.current;
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => setInView(entry?.isIntersecting ?? false),
{ threshold: 0.3 },
);
observer.observe(node);
return () => observer.disconnect();
}, []);
const active = inView && !still;
// The frames lean a few degrees towards the pointer.
const tiltX = useSpring(useMotionValue(0), { stiffness: 120, damping: 18 });
const tiltY = useSpring(useMotionValue(0), { stiffness: 120, damping: 18 });
const onPointerMove = (event: React.PointerEvent) => {
if (still || event.pointerType !== "mouse") return;
const box = event.currentTarget.getBoundingClientRect();
tiltY.set(((event.clientX - box.left) / box.width - 0.5) * 7);
tiltX.set(-((event.clientY - box.top) / box.height - 0.5) * 5);
};
const onPointerLeave = () => {
tiltX.set(0);
tiltY.set(0);
};
return (
{/* Desktop */}
{item.name}
{/* Phone */}
);
};
/**
* WorkShowcase: each live site on a dark stage, as a desktop and a phone view
* that scroll through the real pages.
*/
export const WorkShowcase: React.FC<{ items: readonly WorkItem[] }> = ({
items,
}) => (