"use client"; import { useCallback, useRef, useState, type RefObject } from "react"; import { animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity, type MotionValue, } from "framer-motion"; import ObjectFace from "./ObjectFace"; import { BOARD_SIZE, BOARD_SPOTS, type TischLabels, type TischObject } from "./types"; interface Handle { el: HTMLDivElement; x: MotionValue; y: MotionValue; base: MotionValue; spotRotation: number; } interface BoardProps { objects: TischObject[]; labels: TischLabels; } const TABLE_HEIGHT = 1080; const SETTLE = { type: "spring", bounce: 0, duration: 0.6 } as const; /** Desktop: freier Tisch. Objekte lassen sich werfen, federn am Rand zurück und lassen sich aufräumen. */ export default function Board({ objects, labels }: BoardProps) { const tableRef = useRef(null); const handles = useRef(new Map()); const [order, setOrder] = useState(() => objects.map((o) => o.id)); const bringToFront = useCallback((id: string) => { setOrder((prev) => (prev[prev.length - 1] === id ? prev : [...prev.filter((x) => x !== id), id])); }, []); const tidy = () => { const table = tableRef.current; if (!table) return; const width = table.clientWidth; const gap = 22; let cx = 0; let cy = 28; let rowH = 0; objects.forEach((obj, i) => { const h = handles.current.get(obj.id); if (!h) return; const { w, h: oh } = BOARD_SIZE[obj.kind]; if (cx + w > width && cx > 0) { cx = 0; cy += rowH + gap; rowH = 0; } const tx = cx - h.el.offsetLeft; const ty = cy - h.el.offsetTop; cx += w + gap; rowH = Math.max(rowH, oh); const delay = i * 0.03; animate(h.x, tx, { ...SETTLE, delay }); animate(h.y, ty, { ...SETTLE, delay }); animate(h.base, 0, { ...SETTLE, delay }); }); }; const mess = () => { objects.forEach((obj, i) => { const h = handles.current.get(obj.id); if (!h) return; const delay = i * 0.03; animate(h.x, 0, { type: "spring", bounce: 0.2, duration: 0.6, delay }); animate(h.y, 0, { type: "spring", bounce: 0.2, duration: 0.6, delay }); animate(h.base, h.spotRotation, { type: "spring", bounce: 0.2, duration: 0.6, delay }); }); }; return (

{labels.hintBoard}

{objects.map((obj, i) => ( bringToFront(obj.id)} register={(h) => { if (h) handles.current.set(obj.id, h); else handles.current.delete(obj.id); }} /> ))}
); } interface BoardItemProps { obj: TischObject; labels: TischLabels; spot: { x: number; y: number; r: number }; z: number; tableRef: RefObject; onFront: () => void; register: (h: Handle | null) => void; } function BoardItem({ obj, labels, spot, z, tableRef, onFront, register }: BoardItemProps) { const reduce = useReducedMotion(); const [flipped, setFlipped] = useState(false); const [lifted, setLifted] = useState(false); // Framer meldet nach einem Wurf trotzdem einen Tap; der soll die Karte nicht umdrehen const dragged = useRef(false); const x = useMotionValue(0); const y = useMotionValue(0); const base = useMotionValue(spot.r); // Neigung folgt der Wurfgeschwindigkeit, geglättet über eine Feder const vx = useVelocity(x); const tilt = useSpring(useTransform(vx, [-1600, 0, 1600], [-10, 0, 10]), { stiffness: 300, damping: 30 }); const rotate = useTransform(() => base.get() + (reduce ? 0 : tilt.get())); const { w, h } = BOARD_SIZE[obj.kind]; const canFlip = obj.kind === "project"; const radius = { sticker: "rounded-full", photo: "rounded-md", book: "rounded-[6px_14px_14px_6px]" }[obj.kind as string] ?? "rounded-2xl"; return ( register(el ? { el, x, y, base, spotRotation: spot.r } : null)} drag dragConstraints={tableRef} dragElastic={0.18} dragTransition={{ power: 0.35, timeConstant: 320, bounceStiffness: 260, bounceDamping: 22 }} whileDrag={{ scale: 1.05 }} onPointerDown={() => { dragged.current = false; onFront(); }} onDragStart={() => { dragged.current = true; setLifted(true); }} onDragEnd={() => setLifted(false)} onTap={canFlip ? () => !dragged.current && setFlipped((f) => !f) : undefined} onKeyDown={ canFlip ? (e) => { if (e.key === " ") { e.preventDefault(); setFlipped((f) => !f); } } : undefined } tabIndex={canFlip ? 0 : undefined} role={canFlip ? "button" : undefined} aria-pressed={canFlip ? flipped : undefined} aria-label={canFlip && obj.kind === "project" ? obj.title : undefined} className={`absolute cursor-grab touch-none select-none active:cursor-grabbing ${radius} transition-shadow duration-200 ease-out ${ lifted ? "shadow-[14px_16px_0_rgb(var(--plakat-ink))]" : "shadow-[6px_6px_0_rgb(var(--plakat-ink))]" }`} style={{ x, y, rotate, width: w, height: h, left: `min(${spot.x * 100}%, calc(100% - ${w}px))`, top: spot.y, zIndex: z, }} > ); }