{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "receipt-printer",
  "type": "registry:ui",
  "title": "Receipt Printer",
  "description": "An order card that prints a receipt line by line with totals, a QR payment slot and a tear-off reset.",
  "files": [
    {
      "path": "src/registry/ui/receipt-printer.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { ReceiptText, RotateCcw } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ReceiptAmount = { label: React.ReactNode; amount: number };\n\nexport type ReceiptPayment = {\n  /** Small caption above the code, e.g. \"Scan to pay · UPI\". */\n  label?: React.ReactNode;\n  /** Your QR code (an <svg> or <img>). */\n  code: React.ReactNode;\n  /** Shown under the code, e.g. a UPI ID. */\n  id?: React.ReactNode;\n};\n\nexport type ReceiptPrinterProps = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Shown as the badge next to the title, e.g. \"#4471\". */\n  orderId?: React.ReactNode;\n  merchant: React.ReactNode;\n  /** Lines under the merchant: address, date, table… */\n  details?: React.ReactNode[];\n  items: ReceiptAmount[];\n  /** Added to the subtotal, e.g. service charge and tax. */\n  charges?: ReceiptAmount[];\n  thankYou?: React.ReactNode;\n  payment?: ReceiptPayment;\n  /** ISO 4217 code used to format amounts. */\n  currency?: string;\n  locale?: string;\n  /** Time between printed lines, in ms. */\n  lineInterval?: number;\n  onPrinted?: () => void;\n  onTearOff?: () => void;\n  title?: React.ReactNode;\n};\n\ntype Phase = \"ready\" | \"printing\" | \"printed\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\n\n// Zig-zag torn edges on the top and bottom of the paper.\nconst PAPER_MASK = [\n  \"conic-gradient(from 135deg at top, #0000, #000 1deg 89deg, #0000 90deg) top / 10px 5px repeat-x\",\n  \"conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 10px 5px repeat-x\",\n  \"linear-gradient(#000 0 0) center / 100% calc(100% - 10px) no-repeat\",\n].join(\", \");\n\nexport function ReceiptPrinter({\n  orderId,\n  merchant,\n  details = [],\n  items,\n  charges = [],\n  thankYou = \"Thank you ✦\",\n  payment,\n  currency = \"INR\",\n  locale = \"en-IN\",\n  lineInterval = 140,\n  onPrinted,\n  onTearOff,\n  title = \"Receipt\",\n  className,\n  ...props\n}: ReceiptPrinterProps) {\n  const reduceMotion = useReducedMotion();\n  // Lines fed so far. 0 means the printer is idle; the phase follows from the count.\n  const [printed, setPrinted] = useState(0);\n  // Set when the user acts, so the next control to appear takes focus.\n  const moveFocus = useRef(false);\n  const focusIfRequested = (element: HTMLButtonElement | null) => {\n    if (element && moveFocus.current) {\n      moveFocus.current = false;\n      element.focus();\n    }\n  };\n\n  const money = useMemo(\n    () => new Intl.NumberFormat(locale, { style: \"currency\", currency }),\n    [locale, currency]\n  );\n  const subtotal = items.reduce((sum, item) => sum + item.amount, 0);\n  const total = charges.reduce((sum, charge) => sum + charge.amount, subtotal);\n\n  // Each entry is one printed line; `rule` draws a dashed divider above it.\n  const lines: { node: React.ReactNode; rule?: boolean }[] = [\n    { node: <p className=\"text-center text-base font-bold tracking-[0.2em] text-neutral-900\">{merchant}</p> },\n    ...details.map((detail) => ({\n      node: <p className=\"text-center tracking-wider text-neutral-500 uppercase\">{detail}</p>,\n    })),\n    ...items.map((item, i) => ({ node: <Row {...item} money={money} />, rule: i === 0 })),\n    { node: <Row label=\"Subtotal\" amount={subtotal} money={money} />, rule: true },\n    ...charges.map((charge) => ({ node: <Row {...charge} money={money} /> })),\n    {\n      node: (\n        <Row\n          label=\"TOTAL\"\n          amount={total}\n          money={money}\n          className=\"text-sm font-bold text-neutral-900\"\n        />\n      ),\n    },\n    { node: <p className=\"text-center tracking-[0.15em] text-neutral-800\">{thankYou}</p>, rule: true },\n    ...(payment\n      ? [\n          ...(payment.label\n            ? [{ node: <p className=\"text-center tracking-[0.2em] text-neutral-400 uppercase\">{payment.label}</p> }]\n            : []),\n          {\n            node: (\n              <div className=\"mx-auto w-fit rounded-lg border border-neutral-200 bg-white p-2\">\n                {payment.code}\n              </div>\n            ),\n          },\n          ...(payment.id ? [{ node: <p className=\"text-center text-neutral-700\">{payment.id}</p> }] : []),\n        ]\n      : []),\n  ];\n  const lineCount = lines.length;\n  const phase: Phase = printed === 0 ? \"ready\" : printed < lineCount ? \"printing\" : \"printed\";\n\n  // Feed one line per tick until the receipt is complete.\n  useEffect(() => {\n    if (phase !== \"printing\") return;\n    const timer = setTimeout(() => {\n      const next = printed + 1;\n      setPrinted(next);\n      if (next === lineCount) onPrinted?.();\n    }, lineInterval);\n    return () => clearTimeout(timer);\n  }, [phase, printed, lineCount, lineInterval, onPrinted]);\n\n  const print = () => {\n    moveFocus.current = true;\n    // Reduced motion prints the whole receipt at once.\n    const first = reduceMotion ? lineCount : 1;\n    setPrinted(first);\n    if (first === lineCount) onPrinted?.();\n  };\n  const tearOff = () => {\n    moveFocus.current = true;\n    setPrinted(0);\n    onTearOff?.();\n  };\n\n  const summary = items.map((item, i) => (\n    <span key={i}>\n      {i > 0 && \" · \"}\n      {item.label}\n    </span>\n  ));\n\n  // One timing for every layout change: the card resizing and its parts moving with it.\n  const layoutTransition = reduceMotion\n    ? { duration: 0 }\n    : phase === \"printing\"\n      ? // The paper feeds at a steady rate, one line per tick.\n        { duration: lineInterval / 1000, ease: \"linear\" as const }\n      : { duration: 0.3, ease: EASE_IN_OUT };\n\n  return (\n    <section\n      data-slot=\"receipt-printer\"\n      aria-label={typeof title === \"string\" ? title : \"Receipt\"}\n      className={cn(\"w-full max-w-sm text-foreground\", className)}\n      {...props}\n    >\n      {/* The card animates its size with a layout animation; children opt in so they\n          aren't stretched. Radius lives in style so Motion can correct it while scaling. */}\n      <motion.div\n        layout\n        transition={{ layout: layoutTransition }}\n        style={{ borderRadius: 32 }}\n        className={cn(\n          // The card edge clips the paper: it feeds out while printing and slides out when torn.\n          \"overflow-hidden bg-muted p-2.5 shadow-xl shadow-black/5 ring-1 ring-black/5 dark:ring-white/10\"\n        )}\n      >\n        <motion.header\n          layout=\"position\"\n          transition={{ layout: layoutTransition }}\n          className=\"flex h-12 items-center justify-between gap-3 px-3.5\"\n        >\n          <div className=\"flex items-center gap-2.5\">\n            <h3 className=\"text-lg font-semibold tracking-tight\">{title}</h3>\n            {orderId && (\n              <span className=\"rounded-full bg-foreground/[0.07] px-2.5 py-0.5 font-mono text-xs text-muted-foreground\">\n                {orderId}\n              </span>\n            )}\n          </div>\n          {phase === \"printed\" ? (\n            <button\n              ref={focusIfRequested}\n              type=\"button\"\n              onClick={tearOff}\n              className=\"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full border bg-background px-3.5 text-sm font-medium shadow-sm outline-none transition-[background-color,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-background/70 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97] motion-reduce:active:scale-100\"\n            >\n              <RotateCcw aria-hidden className=\"size-3.5\" />\n              Tear off\n            </button>\n          ) : (\n            <p\n              className={cn(\n                \"text-sm text-muted-foreground\",\n                phase === \"printing\" && \"font-mono tabular-nums\"\n              )}\n            >\n              {phase === \"printing\" ? `printing ${printed}/${lineCount}` : \"Ready\"}\n            </p>\n          )}\n        </motion.header>\n\n        {/* popLayout lifts the leaving view out of the flow, so the next one takes its\n            place immediately and the card resizes while the old one animates away. */}\n        <div className=\"relative\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            {phase === \"ready\" ? (\n              <motion.div\n                key=\"ready\"\n                layout=\"position\"\n                initial={{ opacity: 0 }}\n                // Waits a beat so a torn receipt clears before the panel shows through it.\n                animate={{ opacity: 1, transition: { duration: 0.2, ease: EASE_OUT, delay: 0.1 } }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.2, ease: EASE_OUT, layout: layoutTransition }}\n                className=\"flex flex-col items-center rounded-3xl bg-card px-6 py-10 text-center shadow-sm\"\n              >\n                <span className=\"flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n                  <ReceiptText aria-hidden className=\"size-5\" />\n                </span>\n                <p className=\"mt-5 text-lg font-semibold tracking-tight\">Order ready</p>\n                <p className=\"mt-1 text-sm text-muted-foreground\">{summary}</p>\n                <button\n                  ref={focusIfRequested}\n                  type=\"button\"\n                  onClick={print}\n                  className=\"mt-6 inline-flex h-11 cursor-pointer items-center rounded-full bg-foreground px-6 text-sm font-medium text-background outline-none transition-[opacity,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card active:scale-[0.97] motion-reduce:active:scale-100\"\n                >\n                  Print receipt\n                </button>\n              </motion.div>\n            ) : (\n              <motion.div\n                key=\"paper\"\n                layout=\"position\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                // Torn off the way real receipts are: pulled down against the blade, sliding out\n                // through the bottom of the card while the ready panel fades in beneath it.\n                exit={\n                  reduceMotion\n                    ? { opacity: 0 }\n                    : { opacity: 0, transform: \"translateY(48px) rotate(1.5deg)\" }\n                }\n                transition={{ duration: 0.3, ease: EASE_OUT, layout: layoutTransition }}\n                aria-busy={phase === \"printing\"}\n                // Above its siblings, so it covers the footer as it feeds and stays on top as it leaves.\n                className=\"relative z-10 mx-1 bg-white px-6 py-6 font-mono text-[0.6875rem] text-neutral-600\"\n                style={{ mask: PAPER_MASK, WebkitMask: PAPER_MASK }}\n              >\n                <div className=\"flex flex-col gap-2\">\n                  {lines.slice(0, printed).map((line, i) => (\n                    <div\n                      key={i}\n                      className={cn(\n                        // Each new line inks in as it mounts.\n                        \"transition-[opacity,filter] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] starting:opacity-0 starting:blur-[2px] motion-reduce:starting:blur-none\",\n                        line.rule && \"mt-1 border-t border-dashed border-neutral-300 pt-3\"\n                      )}\n                    >\n                      {line.node}\n                    </div>\n                  ))}\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <motion.p\n          layout=\"position\"\n          transition={{ layout: layoutTransition }}\n          aria-live=\"polite\"\n          className=\"py-4 text-center text-sm text-muted-foreground\"\n        >\n          {phase === \"ready\"\n            ? \"Tap print to issue the receipt\"\n            : phase === \"printing\"\n              ? \"Printing…\"\n              : \"Tear off the receipt to reset\"}\n        </motion.p>\n      </motion.div>\n    </section>\n  );\n}\n\nfunction Row({\n  label,\n  amount,\n  money,\n  className,\n}: ReceiptAmount & { money: Intl.NumberFormat; className?: string }) {\n  return (\n    <p className={cn(\"flex items-baseline justify-between gap-4\", className)}>\n      <span>{label}</span>\n      <span className=\"tabular-nums\">{money.format(amount)}</span>\n    </p>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ]
}
