"use client"; import { useRef, useState, type ReactNode } from "react"; import { animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react"; type SlingShotProps = { children: ReactNode; className?: string; /** Runs on a click, Enter, Space, or a pull that is released. */ onClick?: () => void; /** Colour of the wooden fork. */ wood?: string; /** Colour of the rubber bands. */ band?: string; /** Colour of the stone that flies off. */ stone?: string; }; type Stone = { id: number; x: number; y: number }; const W = 240; const H = 150; const REST = { x: W / 2, y: 62 }; const LEFT = { x: 22, y: 18 }; const RIGHT = { x: W - 22, y: 18 }; const MIN_PULL = 24; /* SlingShot: the button hangs in a catapult. Pull it back and let go, the bands snap and a stone flies. A plain click does a short shot. */ export function SlingShot({ children, className = "rounded-full bg-[#f4b740] px-7 py-3 text-[15px] font-semibold text-[#2a1b05] shadow-[0_6px_14px_rgba(0,0,0,0.45)]", onClick, wood = "#7a4a26", band = "#c8423b", stone = "#f4e7c9", }: SlingShotProps) { const reduce = useReducedMotion(); const x = useMotionValue(0); const y = useMotionValue(0); const [stones, setStones] = useState([]); const next = useRef(0); const dragged = useRef(false); const leftBand = useTransform([x, y], ([dx, dy]: number[]) => `M${LEFT.x} ${LEFT.y} L${REST.x + dx} ${REST.y + dy}`); const rightBand = useTransform([x, y], ([dx, dy]: number[]) => `M${RIGHT.x} ${RIGHT.y} L${REST.x + dx} ${REST.y + dy}`); const thickness = useTransform([x, y], ([dx, dy]: number[]) => Math.max(2.5, 6 - Math.hypot(dx, dy) / 20)); const fire = (dx: number, dy: number) => { onClick?.(); if (reduce) return; const size = Math.hypot(dx, dy) || 1; const reach = 260; setStones((s) => [...s, { id: next.current++, x: (-dx / size) * reach, y: (-dy / size) * reach }]); }; return ( {stones.map((s) => ( setStones((all) => all.filter((o) => o.id !== s.id))} /> ))} { dragged.current = true; }} onDragEnd={() => { const dx = x.get(); const dy = y.get(); if (Math.hypot(dx, dy) >= MIN_PULL) fire(dx, dy); }} onClick={() => { // The browser sends a click after a drag too. The drag already decided. if (dragged.current) { dragged.current = false; return; } fire(0, 30); if (reduce) return; animate(y, [0, 30, 0], { duration: 0.34, times: [0, 0.3, 1], ease: "easeOut" }); }} className={`relative block cursor-grab touch-none whitespace-nowrap active:cursor-grabbing ${className}`} style={{ x, y }} > {children} ); }