"use client"; import { useEffect, useRef, useState } from "react"; import { motion, useInView, useReducedMotion } from "motion/react"; type ReceiptItem = { label: string; price: string }; type ReceiptToastProps = { /** Shop or event name, printed first. */ title?: string; /** Small line under the title. */ meta?: string; /** The lines of the receipt. */ items?: ReceiptItem[]; /** The total, in your own format. */ total?: string; /** Last line, above the barcode. */ footer?: string; /** Tears the paper off by itself this many ms after printing. 0 keeps it. */ duration?: number; /** Called when the paper is torn off. */ onTear?: () => void; /** Set the width here. Everything scales with it. */ className?: string; }; type Row = { kind: "title" | "meta" | "rule" | "item" | "total" | "footer" | "code"; left?: string; right?: string; h: number }; const ITEMS: ReceiptItem[] = [ { label: "Monthly plan", price: "₹499" }, { label: "Extra seat x 2", price: "₹198" }, { label: "Discount", price: "-₹50" }, ]; const LINE = 6.3; // one printed line, in cqw const PAD = 2; const TEETH = 22; const BAR_WIDTHS = [1, 3, 1, 2, 1, 1, 3, 2, 1, 2, 3, 1, 1, 2, 1, 3, 2, 1, 1, 3, 1, 2, 2, 1, 3, 1, 1, 2, 3, 1]; // Both ends of the paper are torn: a saw edge at top and bottom. const EDGE = (() => { const top: string[] = []; const bottom: string[] = []; for (let i = 0; i <= TEETH * 2; i++) { const x = (i / (TEETH * 2)) * 100; top.push(`${x}% ${i % 2 ? "1.1cqw" : "0"}`); bottom.unshift(`${x}% calc(100% - ${i % 2 ? "1.1cqw" : "0px"})`); } return `polygon(${[...top, ...bottom].join(",")})`; })(); /* ReceiptToast: the toast is a slip from a thermal printer. Paper feeds up out of the slot in small jerks, one line at a time, and you drag it up or press Tear to rip it off against the saw bar. */ export function ReceiptToast({ title = "Payment received", meta = "Order 20418 · Paid by UPI", items = ITEMS, total = "₹647", footer = "Thank you. Come again.", duration = 0, onTear, className = "w-full max-w-sm" }: ReceiptToastProps) { const reduce = useReducedMotion(); const box = useRef(null); const seen = useInView(box, { once: true, amount: 0.2 }); const rows: Row[] = [ { kind: "title", left: title, h: 1.5 }, { kind: "meta", left: meta, h: 1 }, { kind: "rule", h: 0.7 }, ...items.map((it): Row => ({ kind: "item", left: it.label, right: it.price, h: 1 })), { kind: "rule", h: 0.7 }, { kind: "total", left: "Total", right: total, h: 1.4 }, { kind: "footer", left: footer, h: 1.1 }, { kind: "code", h: 2 }, ]; const height = rows.reduce((sum, r) => sum + r.h * LINE, PAD * 2); const [run, setRun] = useState(0); // goes up by one for each slip const [printed, setPrinted] = useState(0); const [torn, setTorn] = useState(false); const started = seen && run % 2 === 0; const done = printed >= rows.length; const printing = started && !done && !torn; useEffect(() => { if (!printing) return; const id = setTimeout(() => setPrinted(reduce ? rows.length : (p) => p + 1), reduce ? 0 : 150); return () => clearTimeout(id); }, [printing, printed, reduce, rows.length]); const tear = () => { if (done && !torn) setTorn(true); }; useEffect(() => { if (!done || torn || !duration) return; const id = setTimeout(() => setTorn(true), duration); return () => clearTimeout(id); }, [done, torn, duration]); const out = rows.slice(0, printed).reduce((sum, r) => sum + r.h * LINE, printed ? PAD : 0); const empty = run % 2 === 1; return (
{/* Below the slot nothing shows; above it the paper is free to fly. */}
{ if (info.offset.y < -34) tear(); }} animate={torn ? { y: "-130%", x: "6%", rotate: -7, opacity: 0 } : { y: 0, x: 0, rotate: 0, opacity: 1 }} transition={torn ? { duration: reduce ? 0 : 0.6, ease: [0.3, 0, 0.7, 1] } : { duration: 0 }} onAnimationComplete={() => { if (!torn) return; setTorn(false); setPrinted(0); setRun((r) => r + 1); onTear?.(); }} >
{rows.map((r, i) => (
{r.kind === "rule" &&
} {r.kind === "code" && (
{BAR_WIDTHS.map((w, b) => ( ))}
)} {r.kind === "title" && {r.left}} {(r.kind === "meta" || r.kind === "footer") && {r.left}} {(r.kind === "item" || r.kind === "total") && ( <> {r.left} {r.right} )}
))}
{/* Shadow where the paper leaves the slot. */}
{/* The printer. */} {/* Saw bar with the slot under it. */}
{printing ? "PRINTING" : done ? "READY" : "IDLE"}
{done ? `${title}. ${meta}. ${items.map((i) => `${i.label} ${i.price}`).join(", ")}. Total ${total}. ${footer}` : ""}
); }