"use client"; import { useEffect, useId, useRef, useState, type ReactNode } from "react"; import { animate, motion, useMotionValue, useMotionValueEvent, useReducedMotion } from "motion/react"; type StitchButtonProps = { children: ReactNode; className?: string; onClick?: () => void; /** Colour of the thread. */ thread?: string; /** Share of the edge already sewn at rest, 0 to 1. */ rest?: number; /** Seconds to sew the whole edge. */ duration?: number; }; const INSET = 7; /* StitchButton: a cloth patch, half sewn. Hover and a needle finishes the edge stitch by stitch. Leave and it unpicks. */ export function StitchButton({ children, className = "rounded-[14px] px-10 py-5 text-[15px] font-semibold tracking-wide text-[#f3ead7]", onClick, thread = "#f2c14e", rest = 0.4, duration = 1.1, }: StitchButtonProps) { const reduce = useReducedMotion(); const box = useRef(null); const line = useRef(null); const needle = useRef(null); const [size, setSize] = useState({ w: 0, h: 0 }); const progress = useMotionValue(rest); const maskId = useId(); useEffect(() => { const el = box.current; if (!el) return; const ro = new ResizeObserver(() => setSize({ w: el.offsetWidth, h: el.offsetHeight })); ro.observe(el); return () => ro.disconnect(); }, []); const placeNeedle = (p: number) => { const path = line.current; if (!path || !needle.current || !size.w) return; const total = path.getTotalLength(); const a = path.getPointAtLength(total * p); const b = path.getPointAtLength((total * p + 2) % total); const turn = (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI; needle.current.setAttribute("transform", `translate(${a.x} ${a.y}) rotate(${turn})`); }; useMotionValueEvent(progress, "change", placeNeedle); useEffect(() => placeNeedle(progress.get())); const sew = (on: boolean) => { const target = on ? 1 : rest; if (reduce) progress.set(target); else animate(progress, target, { duration: duration * Math.abs(target - progress.get()), ease: "linear" }); }; const rectProps = { x: INSET, y: INSET, width: Math.max(0, size.w - INSET * 2), height: Math.max(0, size.h - INSET * 2), rx: 8, fill: "none", }; return ( ); }