{
  "name": "receipt-printer",
  "type": "registry:component",
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Receipt Printer\n * @version: 1.0.0\n * @date: 2026-10-14\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  type PointerEvent,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Where the paper comes out, from the top of the card.\nconst SLOT_Y = 64;\n// The printer feeds a line at a time, with a short stop between lines.\nconst LINE = 15;\nconst FEED_RATE = 34;\nconst LINE_PAUSE = 0.055;\nconst LINE_JITTER = 0.05;\n// Simulation, in px and seconds, on a fixed step.\nconst STEP = 1 / 120;\nconst GRAVITY = 2600;\nconst DAMPING = 0.996;\n// While hanging from the slot, a pull stretches only this far, with\n// growing resistance, and tears past this much drag.\nconst MAX_PULL = 18;\nconst TEAR_DRAG = 72;\n// A sharp downward yank tears it too, in px per second.\nconst TEAR_SPEED = 900;\n// A tilted sheet glides sideways as it falls.\nconst GLIDE = 0.35;\nconst REST_EPSILON = 0.02;\n\ntype Phase = \"idle\" | \"printing\" | \"hanging\" | \"held\" | \"falling\";\n\nexport interface ReceiptItem {\n  name: string;\n  quantity: number;\n  price: number;\n}\n\nexport interface ReceiptPrinterProps {\n  merchant?: string;\n  order?: string;\n  date?: string;\n  items?: ReceiptItem[];\n  /** Tax rate, 0.08 for 8%. */\n  tax?: number;\n  currency?: string;\n  /** Print as soon as the printer scrolls into view. */\n  autoPrint?: boolean;\n  onPrint?: () => void;\n  onTear?: () => void;\n  className?: string;\n}\n\nconst DEFAULT_ITEMS: ReceiptItem[] = [\n  { name: \"Flat white\", quantity: 2, price: 4.5 },\n  { name: \"Almond croissant\", quantity: 1, price: 3.8 },\n  { name: \"Oat milk\", quantity: 2, price: 0.6 },\n  { name: \"Banana bread\", quantity: 1, price: 3.2 },\n];\n\n// Barcode bar widths, fixed so server and browser agree.\nconst BARS = [\n  2, 1, 1, 3, 1, 2, 2, 1, 3, 1, 1, 2, 1, 1, 2, 3, 1, 2, 1, 1, 3, 2, 1, 1, 2, 1,\n  3, 1, 1, 2, 2, 1, 1, 3, 1, 2,\n];\n\nfunction damp(distance: number, max: number) {\n  return max * (1 - Math.exp(-distance / max));\n}\n\nfunction haptic(ms = 8) {\n  if (typeof navigator !== \"undefined\" && \"vibrate\" in navigator) {\n    navigator.vibrate(ms);\n  }\n}\n\n/** Local point p on a sheet turned by `angle`, relative to its corner. */\nfunction turn(px: number, py: number, angle: number) {\n  const cos = Math.cos(angle);\n  const sin = Math.sin(angle);\n  return [px * cos - py * sin, px * sin + py * cos];\n}\n\nfunction Receipt({\n  merchant,\n  order,\n  date,\n  items,\n  tax,\n  currency,\n}: Required<\n  Pick<\n    ReceiptPrinterProps,\n    \"merchant\" | \"order\" | \"date\" | \"items\" | \"tax\" | \"currency\"\n  >\n>) {\n  const money = (n: number) => n.toFixed(2);\n  const subtotal = items.reduce((sum, i) => sum + i.quantity * i.price, 0);\n  const taxAmount = subtotal * tax;\n  return (\n    <div className=\"px-5 pt-6 pb-8 font-mono text-[11px] text-zinc-800 leading-[15px]\">\n      <p className=\"text-center font-semibold text-xs tracking-[0.2em]\">\n        {merchant.toUpperCase()}\n      </p>\n      <p className=\"text-center text-zinc-500\">{date}</p>\n      <p className=\"text-center text-zinc-500\">Order {order}</p>\n      <p\n        aria-hidden=\"true\"\n        className=\"my-[7px] overflow-hidden whitespace-nowrap text-zinc-400\"\n      >\n        {\"- \".repeat(40)}\n      </p>\n      {items.map((item) => (\n        <p className=\"flex justify-between gap-3\" key={item.name}>\n          <span className=\"truncate\">\n            {item.quantity} × {item.name}\n          </span>\n          <span className=\"tabular-nums\">\n            {money(item.quantity * item.price)}\n          </span>\n        </p>\n      ))}\n      <p\n        aria-hidden=\"true\"\n        className=\"my-[7px] overflow-hidden whitespace-nowrap text-zinc-400\"\n      >\n        {\"- \".repeat(40)}\n      </p>\n      <p className=\"flex justify-between text-zinc-500\">\n        <span>Subtotal</span>\n        <span className=\"tabular-nums\">{money(subtotal)}</span>\n      </p>\n      <p className=\"flex justify-between text-zinc-500\">\n        <span>Tax {Math.round(tax * 100)}%</span>\n        <span className=\"tabular-nums\">{money(taxAmount)}</span>\n      </p>\n      <p className=\"mt-1 flex justify-between font-semibold text-[13px]\">\n        <span>Total</span>\n        <span className=\"tabular-nums\">\n          {currency}\n          {money(subtotal + taxAmount)}\n        </span>\n      </p>\n      <div aria-hidden=\"true\" className=\"mt-4 flex h-9 justify-center gap-px\">\n        {BARS.map((width, i) => (\n          <span\n            className={i % 2 ? \"bg-transparent\" : \"bg-zinc-800\"}\n            key={`bar-${i.toString()}`}\n            style={{ width }}\n          />\n        ))}\n      </div>\n      <p className=\"mt-3 text-center text-zinc-500\">Thank you, come again</p>\n    </div>\n  );\n}\n\n// Serrated edges: the bottom was torn off the last receipt, and the top\n// gets the same edge once this one is torn.\nconst TOOTH = 8;\nconst edge = (side: \"top\" | \"bottom\") =>\n  `conic-gradient(from ${side === \"bottom\" ? \"-45deg at 50% 100%\" : \"135deg at 50% 0\"}, #0000, #000 1deg 89deg, #0000 90deg) 50% ${side === \"bottom\" ? \"100%\" : \"0\"} / ${TOOTH}px ${TOOTH / 2}px repeat-x`;\nconst FILL = `linear-gradient(#000 0 0) 0 ${TOOTH / 2}px / 100% calc(100% - ${TOOTH}px) no-repeat`;\nconst MASK_RESTING = `${edge(\"bottom\")}, linear-gradient(#000 0 0) 0 0 / 100% calc(100% - ${TOOTH / 2}px) no-repeat`;\nconst MASK_TORN = `${edge(\"top\")}, ${edge(\"bottom\")}, ${FILL}`;\n\nexport default function ReceiptPrinter({\n  merchant = \"Kokonut Coffee\",\n  order = \"#4182\",\n  date = \"14 Oct 2026 · 08:42\",\n  items = DEFAULT_ITEMS,\n  tax = 0.08,\n  currency = \"$\",\n  autoPrint = true,\n  onPrint,\n  onTear,\n  className,\n}: ReceiptPrinterProps) {\n  const [phase, setPhase] = useState<Phase>(\"idle\");\n  const [torn, setTorn] = useState(false);\n\n  const cardRef = useRef<HTMLDivElement>(null);\n  const paperRef = useRef<HTMLDivElement>(null);\n  const printerRef = useRef<HTMLDivElement>(null);\n\n  const sim = useRef({\n    phase: \"idle\" as Phase,\n    width: 0,\n    height: 0,\n    slotX: 0,\n    // Centre of mass, now and one step ago (Verlet), and the sheet's angle.\n    cx: 0,\n    cy: 0,\n    pcx: 0,\n    pcy: 0,\n    angle: 0,\n    spin: 0,\n    // The hinge: a world point the sheet hangs from, and where on the\n    // sheet it is.\n    hx: 0,\n    hy: 0,\n    hingeX: 0,\n    hingeY: 0,\n    // Printing: how much paper is still inside the printer.\n    hidden: 0,\n    feedTarget: 0,\n    pause: 0,\n    // Dragging.\n    pointerId: -1,\n    grabX: 0,\n    grabY: 0,\n    startX: 0,\n    startY: 0,\n    samples: [] as { t: number; x: number; y: number }[],\n    raf: 0,\n    last: 0,\n    carry: 0,\n    visible: false,\n    reduceMotion: false,\n  });\n\n  const callbacks = useRef({ onPrint, onTear });\n  callbacks.current = { onPrint, onTear };\n\n  const setPhaseBoth = useCallback((next: Phase) => {\n    sim.current.phase = next;\n    setPhase(next);\n  }, []);\n\n  const draw = useCallback(() => {\n    const s = sim.current;\n    const paper = paperRef.current;\n    if (!paper) {\n      return;\n    }\n    // Place the sheet so its centre of mass lands on (cx, cy), turned by\n    // `angle` around its top-left corner.\n    const [ox, oy] = turn(s.width / 2, s.height / 2, s.angle);\n    const left = s.slotX - s.width / 2;\n    const tx = s.cx - ox - left;\n    const ty = s.cy - oy - SLOT_Y;\n    paper.style.transform = `translate3d(${tx.toFixed(2)}px,${ty.toFixed(2)}px,0) rotate(${s.angle.toFixed(4)}rad)`;\n  }, []);\n\n  // The centre of mass for a sheet hanging from the hinge at `angle`.\n  const hangAt = useCallback((angle: number) => {\n    const s = sim.current;\n    const [dx, dy] = turn(\n      s.width / 2 - s.hingeX,\n      s.height / 2 - s.hingeY,\n      angle\n    );\n    return [s.hx + dx, s.hy + dy];\n  }, []);\n\n  const placeAtSlot = useCallback(\n    (pull: number) => {\n      const s = sim.current;\n      s.hingeX = s.width / 2;\n      s.hingeY = 0;\n      s.hx = s.slotX;\n      s.hy = SLOT_Y + pull;\n      const [cx, cy] = hangAt(0);\n      s.cx = cx;\n      s.cy = cy;\n      s.pcx = cx;\n      s.pcy = cy;\n      s.angle = 0;\n    },\n    [hangAt]\n  );\n\n  // A thermal printer moves in short bursts: feed a line, stop, feed.\n  const stepPrinting = useCallback(\n    (dt: number) => {\n      const s = sim.current;\n      if (s.pause > 0) {\n        s.pause -= dt;\n      } else if (s.feedTarget > 0) {\n        s.feedTarget = Math.max(0, s.feedTarget - LINE);\n        s.pause = LINE_PAUSE + Math.random() * LINE_JITTER;\n      }\n      s.hidden += (s.feedTarget - s.hidden) * Math.min(1, FEED_RATE * dt);\n      if (Math.abs(s.hidden - s.feedTarget) < 0.05) {\n        s.hidden = s.feedTarget;\n      }\n      placeAtSlot(-s.hidden);\n      const printer = printerRef.current;\n      if (printer) {\n        // The housing buzzes while the motor runs.\n        const buzz = s.pause > 0 ? 0 : Math.sin(performance.now() / 9) * 0.4;\n        printer.style.transform = `translate3d(${buzz.toFixed(2)}px,0,0)`;\n      }\n      if (s.hidden === 0 && s.feedTarget === 0) {\n        if (printer) {\n          printer.style.transform = \"\";\n        }\n        setPhaseBoth(\"hanging\");\n        return 0;\n      }\n      return 1;\n    },\n    [placeAtSlot, setPhaseBoth]\n  );\n\n  // Hanging from a hinge: the centre of mass swings on a fixed-length\n  // rod, and the sheet turns with it.\n  const stepPendulum = useCallback(() => {\n    const s = sim.current;\n    const vx = (s.cx - s.pcx) * DAMPING;\n    const vy = (s.cy - s.pcy) * DAMPING;\n    s.pcx = s.cx;\n    s.pcy = s.cy;\n    s.cx += vx;\n    s.cy += vy + GRAVITY * STEP * STEP;\n    const rod = Math.hypot(s.width / 2 - s.hingeX, s.height / 2 - s.hingeY);\n    const dx = s.cx - s.hx;\n    const dy = s.cy - s.hy;\n    const dist = Math.hypot(dx, dy) || 1;\n    s.cx = s.hx + (dx / dist) * rod;\n    s.cy = s.hy + (dy / dist) * rod;\n    const rest = Math.atan2(s.height / 2 - s.hingeY, s.width / 2 - s.hingeX);\n    s.angle = Math.atan2(s.cy - s.hy, s.cx - s.hx) - rest;\n    return Math.abs(vx) + Math.abs(vy);\n  }, []);\n\n  // Free fall: gravity, a little air, and a tilted sheet drifts sideways.\n  const stepFalling = useCallback(() => {\n    const s = sim.current;\n    let vx = (s.cx - s.pcx) * DAMPING;\n    const vy = (s.cy - s.pcy) * DAMPING;\n    vx += Math.sin(s.angle) * Math.max(vy, 0) * GLIDE * STEP * 10;\n    s.pcx = s.cx;\n    s.pcy = s.cy;\n    s.cx += vx;\n    s.cy += vy + GRAVITY * STEP * STEP;\n    s.angle += s.spin * STEP;\n    s.spin *= 0.995;\n    // Off the bottom of the card: done.\n    const card = cardRef.current;\n    const floor =\n      (card?.clientHeight ?? 400) + Math.hypot(s.width, s.height) / 2;\n    return s.cy > floor ? 0 : 1;\n  }, []);\n\n  const stepPhysics = useCallback(\n    (elapsed: number) => {\n      const s = sim.current;\n      let busy = 0;\n      s.carry += elapsed;\n      while (s.carry >= STEP) {\n        s.carry -= STEP;\n        if (s.phase === \"falling\") {\n          busy = stepFalling();\n        } else if (s.phase === \"hanging\" || s.phase === \"held\") {\n          busy = stepPendulum();\n        }\n      }\n      return busy;\n    },\n    [stepFalling, stepPendulum]\n  );\n\n  const finishFall = useCallback(() => {\n    setPhaseBoth(\"idle\");\n  }, [setPhaseBoth]);\n\n  const tick = useCallback(\n    (now: number) => {\n      const s = sim.current;\n      s.raf = 0;\n      const elapsed = s.last ? Math.min((now - s.last) / 1000, 0.05) : STEP;\n      s.last = now;\n      const busy =\n        s.phase === \"printing\" ? stepPrinting(elapsed) : stepPhysics(elapsed);\n      draw();\n      if (s.phase === \"falling\" && busy === 0) {\n        finishFall();\n        s.last = 0;\n        return;\n      }\n      const keepGoing =\n        s.phase === \"printing\" ||\n        s.phase === \"falling\" ||\n        s.pointerId >= 0 ||\n        busy > REST_EPSILON;\n      if (keepGoing && s.visible) {\n        s.raf = requestAnimationFrame(tick);\n      } else {\n        s.last = 0;\n      }\n    },\n    [draw, finishFall, stepPhysics, stepPrinting]\n  );\n\n  const wake = useCallback(() => {\n    const s = sim.current;\n    if (!s.raf && s.visible) {\n      s.raf = requestAnimationFrame(tick);\n    }\n  }, [tick]);\n\n  const measure = useCallback(() => {\n    const s = sim.current;\n    const card = cardRef.current;\n    const paper = paperRef.current;\n    if (!(card && paper)) {\n      return;\n    }\n    s.width = paper.offsetWidth;\n    s.height = paper.offsetHeight;\n    s.slotX = card.clientWidth / 2;\n  }, []);\n\n  const print = useCallback(() => {\n    const s = sim.current;\n    measure();\n    setTorn(false);\n    callbacks.current.onPrint?.();\n    if (s.reduceMotion) {\n      placeAtSlot(0);\n      draw();\n      setPhaseBoth(\"hanging\");\n      return;\n    }\n    s.hidden = s.height;\n    s.feedTarget = s.height;\n    s.pause = 0.25;\n    placeAtSlot(-s.hidden);\n    draw();\n    setPhaseBoth(\"printing\");\n    wake();\n  }, [draw, measure, placeAtSlot, setPhaseBoth, wake]);\n\n  const tear = useCallback(\n    (held: boolean) => {\n      const s = sim.current;\n      setTorn(true);\n      haptic(14);\n      callbacks.current.onTear?.();\n      const printer = printerRef.current;\n      printer?.animate(\n        [\n          { transform: \"translate3d(0,1.5px,0)\" },\n          { transform: \"translate3d(0,0,0)\" },\n        ],\n        { duration: 220, easing: \"cubic-bezier(0.23, 1, 0.32, 1)\" }\n      );\n      if (s.reduceMotion) {\n        setPhaseBoth(\"idle\");\n        return;\n      }\n      setPhaseBoth(held ? \"held\" : \"falling\");\n      wake();\n    },\n    [setPhaseBoth, wake]\n  );\n\n  // Print once it scrolls into view; pause the loop when it's offscreen.\n  useEffect(() => {\n    const s = sim.current;\n    const card = cardRef.current;\n    if (!card) {\n      return;\n    }\n    s.reduceMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    measure();\n    placeAtSlot(-s.height);\n    draw();\n    let printed = false;\n    let onScreen = false;\n    const update = () => {\n      s.visible = onScreen && !document.hidden;\n      if (!s.visible) {\n        cancelAnimationFrame(s.raf);\n        s.raf = 0;\n        s.last = 0;\n        return;\n      }\n      if (autoPrint && !printed) {\n        printed = true;\n        print();\n      }\n      wake();\n    };\n    const intersection = new IntersectionObserver(\n      ([entry]) => {\n        onScreen = entry.isIntersecting;\n        update();\n      },\n      { threshold: 0.4 }\n    );\n    intersection.observe(card);\n    document.addEventListener(\"visibilitychange\", update);\n    return () => {\n      intersection.disconnect();\n      document.removeEventListener(\"visibilitychange\", update);\n      cancelAnimationFrame(s.raf);\n      s.raf = 0;\n    };\n  }, [autoPrint, draw, measure, placeAtSlot, print, wake]);\n\n  const localPoint = (event: PointerEvent) => {\n    const rect = cardRef.current?.getBoundingClientRect();\n    return rect\n      ? { x: event.clientX - rect.left, y: event.clientY - rect.top }\n      : { x: 0, y: 0 };\n  };\n\n  // A world point to where it sits on the sheet.\n  const toSheet = (x: number, y: number) => {\n    const s = sim.current;\n    const [ox, oy] = turn(s.width / 2, s.height / 2, s.angle);\n    const cornerX = s.cx - ox;\n    const cornerY = s.cy - oy;\n    return turn(x - cornerX, y - cornerY, -s.angle);\n  };\n\n  const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {\n    const s = sim.current;\n    if (\n      s.phase !== \"hanging\" ||\n      (event.pointerType === \"mouse\" && event.button !== 0)\n    ) {\n      return;\n    }\n    const { x, y } = localPoint(event);\n    event.currentTarget.setPointerCapture(event.pointerId);\n    s.pointerId = event.pointerId;\n    s.startX = x;\n    s.startY = y;\n    const [gx, gy] = toSheet(x, y);\n    s.grabX = gx;\n    s.grabY = gy;\n    s.samples = [{ t: event.timeStamp, x, y }];\n    wake();\n  };\n\n  // Still attached: the sheet turns about the slot so the grabbed point\n  // follows the hand sideways, and stretches a little downward.\n  const dragAttached = (x: number, y: number) => {\n    const s = sim.current;\n    const pull = damp(Math.max(0, y - s.startY), MAX_PULL);\n    s.hingeX = s.width / 2;\n    s.hingeY = 0;\n    s.hx = s.slotX;\n    s.hy = SLOT_Y + pull;\n    const reach = Math.max(s.grabY, 24);\n    const angle = -Math.atan2(x - s.slotX - (s.grabX - s.width / 2), reach);\n    const [cx, cy] = hangAt(angle * 0.7);\n    s.pcx = s.cx;\n    s.pcy = s.cy;\n    s.cx = cx;\n    s.cy = cy;\n    s.angle = angle * 0.7;\n  };\n\n  const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {\n    const s = sim.current;\n    if (s.pointerId !== event.pointerId) {\n      return;\n    }\n    const { x, y } = localPoint(event);\n    s.samples.push({ t: event.timeStamp, x, y });\n    if (s.samples.length > 5) {\n      s.samples.shift();\n    }\n    if (s.phase === \"hanging\") {\n      if (y - s.startY > TEAR_DRAG) {\n        // It rips at the slot and stays in the hand, swinging from where\n        // it was grabbed.\n        s.hingeX = s.grabX;\n        s.hingeY = s.grabY;\n        s.hx = x;\n        s.hy = y;\n        tear(true);\n      } else {\n        dragAttached(x, y);\n      }\n    } else if (s.phase === \"held\") {\n      s.hx = x;\n      s.hy = y;\n    }\n    wake();\n  };\n\n  const releaseVelocity = () => {\n    const s = sim.current;\n    const first = s.samples[0];\n    const last = s.samples.at(-1) ?? first;\n    const dt = Math.max((last.t - first.t) / 1000, 0.016);\n    return { vx: (last.x - first.x) / dt, vy: (last.y - first.y) / dt };\n  };\n\n  const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {\n    const s = sim.current;\n    if (s.pointerId !== event.pointerId) {\n      return;\n    }\n    s.pointerId = -1;\n    const { vx, vy } = releaseVelocity();\n    if (s.phase === \"held\") {\n      // Thrown: keep the hand's speed, and spin from the swing.\n      const prevAngle = s.angle;\n      stepPendulum();\n      s.spin = (s.angle - prevAngle) / STEP + vx * 0.002;\n      s.pcx = s.cx - vx * STEP * 0.6;\n      s.pcy = s.cy - vy * STEP * 0.6;\n      setPhaseBoth(\"falling\");\n    } else if (s.phase === \"hanging\" && vy > TEAR_SPEED) {\n      // A sharp yank tears it straight off.\n      s.pcx = s.cx - vx * STEP * 0.5;\n      s.pcy = s.cy - vy * STEP * 0.5;\n      s.spin = vx * 0.004;\n      tear(false);\n    } else if (s.phase === \"hanging\") {\n      // Let go: it springs back up to the slot and the swing plays out.\n      s.hy = SLOT_Y;\n      s.pcx = s.cx;\n      s.pcy = s.cy;\n    }\n    wake();\n  };\n\n  const handleTearKey = () => {\n    const s = sim.current;\n    if (s.phase === \"hanging\") {\n      s.spin = 1.2;\n      s.pcy = s.cy - 3;\n      tear(false);\n    }\n  };\n\n  const busy = phase === \"printing\";\n  let status = \"Ready\";\n  if (busy) {\n    status = \"Printing…\";\n  } else if (phase === \"hanging\") {\n    status = \"Pull down to tear\";\n  }\n\n  return (\n    <div\n      className={cn(\n        \"relative h-[30rem] w-72 select-none overflow-hidden rounded-3xl border border-zinc-200 bg-zinc-50 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100\",\n        className\n      )}\n      ref={cardRef}\n    >\n      {/* The sheet, clipped at the slot until it's torn free. */}\n      <div\n        className={cn(\n          \"absolute inset-x-0 bottom-0\",\n          torn ? \"overflow-visible\" : \"overflow-hidden\"\n        )}\n        style={{ top: SLOT_Y }}\n      >\n        <div\n          aria-label={`Receipt from ${merchant}, order ${order}`}\n          className={cn(\n            \"absolute top-0 w-56 bg-white shadow-[0_1px_2px_rgb(0_0_0/0.06),0_8px_24px_-12px_rgb(0_0_0/0.25)] outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:bg-zinc-100\",\n            phase === \"hanging\" && \"cursor-grab active:cursor-grabbing\",\n            phase === \"held\" && \"cursor-grabbing\",\n            phase === \"idle\" && torn && \"invisible\"\n          )}\n          onLostPointerCapture={handlePointerUp}\n          onPointerCancel={handlePointerUp}\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={handlePointerUp}\n          ref={paperRef}\n          role=\"document\"\n          style={{\n            left: \"50%\",\n            marginLeft: \"-7rem\",\n            transformOrigin: \"0 0\",\n            mask: torn ? MASK_TORN : MASK_RESTING,\n            WebkitMask: torn ? MASK_TORN : MASK_RESTING,\n            touchAction: \"none\",\n          }}\n        >\n          {/* Perforation along the slot, where the sheet tears off. */}\n          {!torn && (\n            <span\n              aria-hidden=\"true\"\n              className=\"absolute inset-x-1 top-1 border-zinc-300 border-t-2 border-dotted\"\n            />\n          )}\n          <Receipt\n            currency={currency}\n            date={date}\n            items={items}\n            merchant={merchant}\n            order={order}\n            tax={tax}\n          />\n        </div>\n      </div>\n\n      {/* The printer housing, in front of the paper's top edge. */}\n      <div\n        className=\"absolute inset-x-0 top-0 h-16 border-zinc-200 border-b bg-white dark:border-zinc-800 dark:bg-zinc-900\"\n        ref={printerRef}\n      >\n        <div className=\"flex items-center justify-between px-5 pt-4 text-xs\">\n          <span className=\"font-medium\">{merchant}</span>\n          <span aria-live=\"polite\" className=\"text-zinc-500 dark:text-zinc-400\">\n            {status}\n          </span>\n        </div>\n        <div className=\"absolute inset-x-8 bottom-2 h-1 rounded-full bg-zinc-900 dark:bg-black\" />\n      </div>\n\n      {phase === \"hanging\" && (\n        <button\n          className=\"sr-only focus:not-sr-only focus:absolute focus:right-4 focus:bottom-4 focus:rounded-full focus:bg-zinc-900 focus:px-3 focus:py-1.5 focus:text-white focus:text-xs dark:focus:bg-zinc-100 dark:focus:text-zinc-900\"\n          onClick={handleTearKey}\n          type=\"button\"\n        >\n          Tear off receipt\n        </button>\n      )}\n\n      <div\n        className={cn(\n          \"absolute inset-x-0 bottom-0 flex justify-center p-5 transition-opacity duration-200 ease-out\",\n          phase === \"idle\" ? \"opacity-100\" : \"pointer-events-none opacity-0\"\n        )}\n      >\n        <button\n          className=\"rounded-full bg-zinc-900 px-4 py-2 font-medium text-sm text-white outline-none transition-colors duration-150 ease-out hover:bg-zinc-700 focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300\"\n          disabled={phase !== \"idle\"}\n          onClick={print}\n          tabIndex={phase === \"idle\" ? 0 : -1}\n          type=\"button\"\n        >\n          {torn ? \"Print another\" : \"Print receipt\"}\n        </button>\n      </div>\n    </div>\n  );\n}\n",
      "path": "/components/kokonutui/receipt-printer.tsx",
      "target": "components/kokonutui/receipt-printer.tsx"
    }
  ]
}