{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "receipt-printer",
  "title": "Receipt printer",
  "description": "A small checkout terminal: pay, wait for the card, then the receipt rolls out of the slot.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/receipt-printer/receipt-printer.tsx",
      "content": "import { Check, LoaderCircle, RotateCcw } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useState } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type ReceiptLine = { label: string; amount: number }\n\nexport type ReceiptPrinterProps = {\n  merchant: string\n  lines: ReceiptLine[]\n  /** Called when the pay button is pressed. The receipt prints once it resolves; if it throws, the button comes back. */\n  onPay: () => Promise<{ reference: string } | void>\n  currency?: string\n  locale?: string\n  labels?: Partial<Record<'pay' | 'processing' | 'printing' | 'paid' | 'total' | 'again' | 'reference', string>>\n  className?: string\n}\n\ntype Phase = 'idle' | 'processing' | 'printing' | 'paid'\n\nconst EN = { pay: 'Pay', processing: 'Processing payment', printing: 'Printing your receipt', paid: 'Paid', total: 'Total', again: 'New order', reference: 'Ref' }\n\n/** A small checkout terminal: pay, wait for the card, then the receipt rolls out of the slot. */\nexport function ReceiptPrinter({ merchant, lines, onPay, currency = 'EUR', locale = 'en-IE', labels = {}, className }: ReceiptPrinterProps) {\n  const t = { ...EN, ...labels }\n  const [phase, setPhase] = useState<Phase>('idle')\n  const [ref, setRef] = useState<string | null>(null)\n  const [stamp, setStamp] = useState('')\n  const money = new Intl.NumberFormat(locale, { style: 'currency', currency })\n  const total = lines.reduce((sum, l) => sum + l.amount, 0)\n\n  const pay = async () => {\n    setPhase('processing')\n    try {\n      const r = await onPay()\n      setRef(r?.reference ?? null)\n      setStamp(new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(new Date()))\n      setPhase('printing')\n    } catch {\n      setPhase('idle')\n    }\n  }\n\n  const status = { idle: null, processing: t.processing, printing: t.printing, paid: t.paid }[phase]\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div className={cn('flex w-full min-w-0 max-w-sm flex-col items-center', className)}>\n        {/* The terminal */}\n        <div className=\"relative z-10 w-full rounded-[28px] bg-neutral-800 p-3 shadow-[0_18px_40px_-18px_rgb(0_0_0/0.6)] dark:bg-neutral-900 dark:ring-1 dark:ring-white/10\">\n          <div className=\"rounded-2xl bg-neutral-950 p-4 text-neutral-50\">\n            <div className=\"flex items-start justify-between gap-3\">\n              <div className=\"min-w-0\">\n                <p className=\"truncate font-semibold tracking-wide uppercase\">{merchant}</p>\n                <p className=\"truncate text-sm text-neutral-400\">{lines.map((l) => l.label).join(', ')}</p>\n              </div>\n              <div className=\"text-right\">\n                <p className=\"text-sm text-neutral-400\">{t.total}</p>\n                <p className=\"font-mono text-xl font-semibold tabular-nums\">{money.format(total)}</p>\n              </div>\n            </div>\n\n            <div className=\"mt-4 min-h-11\" aria-live=\"polite\">\n              <AnimatePresence mode=\"popLayout\" initial={false}>\n                {phase === 'idle' ? (\n                  <motion.button\n                    key=\"pay\"\n                    type=\"button\"\n                    onClick={pay}\n                    initial={{ opacity: 0, scale: 0.96 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0.96 }}\n                    transition={{ duration: 0.2 }}\n                    className=\"min-h-11 w-full rounded-xl bg-neutral-50 font-semibold text-neutral-950 transition-transform duration-150 active:scale-[0.98]\"\n                  >\n                    {t.pay} {money.format(total)}\n                  </motion.button>\n                ) : (\n                  <motion.p\n                    key={phase}\n                    initial={{ opacity: 0, y: 6 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -6 }}\n                    transition={{ duration: 0.2 }}\n                    className=\"flex min-h-11 items-center gap-2 text-sm text-neutral-300\"\n                  >\n                    {phase === 'paid' ? <Check className=\"size-4 text-emerald-400\" aria-hidden /> : <LoaderCircle className=\"size-4 animate-spin motion-reduce:animate-none\" aria-hidden />}\n                    {status}\n                    {phase === 'paid' && (\n                      <button\n                        type=\"button\"\n                        onClick={() => setPhase('idle')}\n                        className=\"ml-auto inline-flex min-h-10 items-center gap-1.5 rounded-lg px-2.5 text-neutral-300 transition-colors duration-150 hover:bg-white/10 hover:text-white\"\n                      >\n                        <RotateCcw className=\"size-3.5\" aria-hidden />\n                        {t.again}\n                      </button>\n                    )}\n                  </motion.p>\n                )}\n              </AnimatePresence>\n            </div>\n          </div>\n          {/* The slot */}\n          <div className=\"mx-4 mt-3 h-2 rounded-full bg-black shadow-[inset_0_1px_2px_rgb(0_0_0/0.8)]\" />\n        </div>\n\n        {/* The paper comes out of the slot: it starts hidden above the clip edge and slides down. */}\n        <div className=\"-mt-3 w-[86%] overflow-hidden pt-1\">\n          <AnimatePresence>\n            {(phase === 'printing' || phase === 'paid') && (\n              <motion.div\n                key=\"receipt\"\n                initial={{ y: '-100%' }}\n                animate={{ y: 0 }}\n                exit={{ opacity: 0, transition: { duration: 0.2 } }}\n                transition={{ duration: 1.8, ease: [0.4, 0, 0.3, 1] }}\n                onAnimationComplete={() => setPhase((p) => (p === 'printing' ? 'paid' : p))}\n                className=\"bg-[#f3eee3] px-5 pt-6 pb-4 font-mono text-[13px] text-neutral-800 [mask:radial-gradient(6px_at_50%_100%,#0000_98%,#000)_50%_0/12px_100%_repeat-x]\"\n              >\n                <p className=\"text-center text-base font-bold tracking-widest uppercase\">{merchant}</p>\n                <p className=\"mt-1 text-center text-xs text-neutral-500\">{stamp}</p>\n                <div className=\"my-3 border-t border-dashed border-neutral-400\" />\n                <ul className=\"space-y-1\">\n                  {lines.map((l, i) => (\n                    <li key={i} className=\"flex justify-between gap-3\">\n                      <span className=\"truncate\">{l.label}</span>\n                      <span className=\"tabular-nums\">{money.format(l.amount)}</span>\n                    </li>\n                  ))}\n                </ul>\n                <div className=\"my-3 border-t border-dashed border-neutral-400\" />\n                <p className=\"flex justify-between font-bold\">\n                  <span>{t.total}</span>\n                  <span className=\"tabular-nums\">{money.format(total)}</span>\n                </p>\n                {ref && (\n                  <p className=\"mt-2 text-xs text-neutral-500\">\n                    {t.reference} {ref}\n                  </p>\n                )}\n                {/* A decorative barcode from the reference, so each receipt looks a little different. */}\n                <div aria-hidden className=\"mt-4 flex h-10 items-stretch justify-center gap-0.5 pb-2\">\n                  {Array.from((ref ?? merchant).padEnd(14, '0').slice(0, 14)).flatMap((c, i) => {\n                    const code = c.charCodeAt(0)\n                    return [<span key={`${i}a`} className=\"bg-neutral-800\" style={{ width: 1 + (code % 3) }} />, <span key={`${i}b`} className=\"bg-neutral-800\" style={{ width: 1 + ((code >> 2) % 2) }} />]\n                  })}\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}