"use client"; import { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from "motion/react"; import { useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; export type EnvelopeHue = "sky" | "emerald" | "amber" | "violet" | "rose" | "orange" | "indigo" | "teal"; const hues: Record = { sky: "[--seal:var(--color-sky-600)] [--seal-hi:var(--color-sky-400)] [--seal-lo:var(--color-sky-800)]", emerald: "[--seal:var(--color-emerald-600)] [--seal-hi:var(--color-emerald-400)] [--seal-lo:var(--color-emerald-800)]", amber: "[--seal:var(--color-amber-600)] [--seal-hi:var(--color-amber-400)] [--seal-lo:var(--color-amber-800)]", violet: "[--seal:var(--color-violet-600)] [--seal-hi:var(--color-violet-400)] [--seal-lo:var(--color-violet-800)]", rose: "[--seal:var(--color-rose-600)] [--seal-hi:var(--color-rose-400)] [--seal-lo:var(--color-rose-800)]", orange: "[--seal:var(--color-orange-600)] [--seal-hi:var(--color-orange-400)] [--seal-lo:var(--color-orange-800)]", indigo: "[--seal:var(--color-indigo-600)] [--seal-hi:var(--color-indigo-400)] [--seal-lo:var(--color-indigo-800)]", teal: "[--seal:var(--color-teal-600)] [--seal-hi:var(--color-teal-400)] [--seal-lo:var(--color-teal-800)]", }; type Stage = "sealed" | "unsealed" | "open" | "out"; export interface EnvelopeProps { hue?: EnvelopeHue; title?: string; message?: string; from?: string; monogram?: string; autoPlay?: boolean; className?: string; } const SW = 400; const SH = 300; const EX = 50; const EY = 124; const EW = 300; const EH = 172; const FLAP_H = 104; function useFit(width: number) { const ref = useRef(null); const [k, setK] = useState(1); useLayoutEffect(() => { const el = ref.current; if (!el) return; const measure = () => setK(el.offsetWidth / width); measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [width]); return [ref, k] as const; } const order: Stage[] = ["sealed", "unsealed", "open", "out"]; function dwell(from: Stage, to: Stage) { if (from === "unsealed") return 260; if (from === "open" && to === "out") return 420; if (from === "open" && to === "sealed") return 400; return 0; } function useCalmMotion() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } export function Envelope({ hue = "rose", title = "You're invited", message = "Join us for the Northwind launch on Friday, 6 pm, at the old boathouse.", from = "— Ines & the team", monogram = "N", autoPlay = true, className, }: EnvelopeProps) { const reduce = useCalmMotion(); const root = useRef(null); const inView = useInView(root); const [fitRef, k] = useFit(SW); const liningId = `lining-${useId().replace(/:/g, "")}`; const [stage, setStage] = useState("sealed"); const [target, setTarget] = useState("sealed"); const [touched, setTouched] = useState(false); useEffect(() => { if (stage === target) return; const forward = order.indexOf(target) > order.indexOf(stage); const next: Stage = forward ? order[order.indexOf(stage) + 1] : stage === "out" ? "open" : "sealed"; const t = setTimeout(() => setStage(next), reduce ? 0 : dwell(stage, next)); return () => clearTimeout(t); }, [stage, target, reduce]); useEffect(() => { if (!autoPlay || touched || !inView || stage !== target) return; const t = setTimeout(() => setTarget(target === "out" ? "sealed" : "out"), target === "out" ? 3200 : 1800); return () => clearTimeout(t); }, [autoPlay, touched, inView, stage, target]); const click = () => { setTouched(true); setTarget(target === "out" ? "sealed" : "out"); }; const flapOpen = stage === "open" || stage === "out"; const sealOn = stage === "sealed"; const letterUp = stage === "out"; const flap = useMotionValue(0); useEffect(() => { const c = animate(flap, flapOpen ? 180 : 0, reduce ? { duration: 0 } : { duration: 0.5, ease: [0.65, 0, 0.35, 1] }); return () => c.stop(); }, [flapOpen, reduce, flap]); const flapZ = useTransform(flap, (r) => (r > 90 ? 1 : 30)); const flapShade = useTransform(flap, [0, 90, 180], [0, 0.18, 0]); return (
); }