"use client"; import { AnimatePresence, motion, useAnimate, useReducedMotion } from "motion/react"; import { createContext, type ReactNode, useCallback, useContext, useEffect, useId, useState } from "react"; import { cn } from "@/lib/utils"; export type InkStampTone = "emerald" | "rose" | "sky" | "violet" | "amber"; const tones: Record = { emerald: "text-emerald-600 dark:text-emerald-400", rose: "text-rose-600 dark:text-rose-400", sky: "text-sky-600 dark:text-sky-400", violet: "text-violet-600 dark:text-violet-400", amber: "text-amber-600 dark:text-amber-400", }; const ShakeContext = createContext<(() => void) | null>(null); function useSafeReducedMotion() { const prefers = useReducedMotion(); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); return mounted && !!prefers; } function hash(value: string) { let h = 2166136261; for (let i = 0; i < value.length; i++) h = Math.imul(h ^ value.charCodeAt(i), 16777619); return h >>> 0; } function random(seed: number) { let s = seed || 1; return () => { s = Math.imul(s ^ (s >>> 15), 2246822507) ^ Math.imul(s ^ (s >>> 13), 3266489909); return ((s ^= s >>> 16) >>> 0) / 4294967296; }; } export interface StampSurfaceProps { children: ReactNode; className?: string; } export function StampSurface({ children, className }: StampSurfaceProps) { const [scope, animate] = useAnimate(); const reduce = useSafeReducedMotion(); const shake = useCallback(() => { if (reduce || !scope.current) return; animate( scope.current, { x: [0, -2.5, 2, -1.2, 0.5, 0], y: [0, 2, -1, 0.5, 0, 0] }, { duration: 0.3, ease: "easeOut" }, ); }, [animate, reduce, scope]); return (
{children}
); } export interface InkStampProps { label: string; caption?: string; tone?: InkStampTone; rotate?: number; stampKey?: string | number | null; onImpact?: () => void; className?: string; } export function InkStamp({ label, caption, tone = "emerald", rotate = -8, stampKey, onImpact, className }: InkStampProps) { const shake = useContext(ShakeContext); const reduce = useSafeReducedMotion(); const filterBase = `ink-stamp-${useId().replace(/:/g, "")}`; const impact = useCallback(() => { shake?.(); onImpact?.(); }, [shake, onImpact]); return (
{stampKey == null ? "" : `Stamped ${label}${caption ? `, ${caption}` : ""}`} {stampKey != null && ( )}
); } interface ImpressionProps { label: string; caption?: string; rotate: number; seed: number; filterId: string; reduce: boolean; onImpact: () => void; } const SLAM = 0.36; function Impression({ label, caption, rotate, seed, filterId, reduce, onImpact }: ImpressionProps) { const [rand] = useState(() => random(seed)); const [shape] = useState(() => ({ tilt: rotate + (rand() - 0.5) * 4, dx: (rand() - 0.5) * 6, dy: (rand() - 0.5) * 4, specks: Array.from({ length: 5 }, () => ({ left: `${rand() * 100}%`, top: `${rand() < 0.5 ? -10 - rand() * 14 : 110 + rand() * 14}%`, size: 1.5 + rand() * 2.5, })), warp: Math.floor(rand() * 900), grain: Math.floor(rand() * 900), })); useEffect(() => { if (reduce) return; const timer = setTimeout(onImpact, SLAM * 1000); return () => clearTimeout(timer); }, [onImpact, reduce]); const ink = reduce ? { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 } } : { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.04, delay: SLAM } }; const face = (
{label} {caption ? ( {caption} ) : null}
); return ( {face} {face} {shape.specks.map((s) => ( ))} {!reduce && } ); } function StampBlock() { const times = [0, 0.2, 0.37, 0.52, 0.58, 0.63, 1]; return ( ); }