{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "receipt",
  "type": "registry:ui",
  "title": "Receipt",
  "description": "A thermal-paper receipt with torn edges that prints line by line.",
  "files": [
    {
      "path": "ui/receipt.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type ReceiptItem = { name: string; price: number; qty?: number };\n\nexport interface ReceiptProps extends React.HTMLAttributes<HTMLDivElement> {\n  title?: string;\n  subtitle?: string;\n  meta?: { label: string; value: string }[];\n  items: ReceiptItem[];\n  currency?: string;\n  /** e.g. 0.08 for 8% tax */\n  taxRate?: number;\n  footer?: string;\n  /** Barcode value. Pass false to hide. */\n  barcode?: string | false;\n  /** Animate the receipt printing line by line when it scrolls into view. */\n  print?: boolean;\n  width?: number;\n}\n\nconst NOISE =\n  \"url(\\\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 .09 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>\\\")\";\n\nconst ZIGZAG =\n  \"conic-gradient(from 135deg at top, #0000, #000 1deg 89deg, #0000 90deg) top / 12px 51% repeat-x, conic-gradient(from -45deg at bottom, #0000, #000 1deg 89deg, #0000 90deg) bottom / 12px 51% repeat-x\";\n\nconst CSS = `\n@keyframes eph-receipt-print { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); } }\n.eph-receipt-hidden { clip-path: inset(0 0 100% 0); }\n@media (prefers-reduced-motion: reduce) {\n  .eph-receipt-hidden { clip-path: none; }\n  .eph-receipt-printing { animation: none !important; }\n}\n`;\n\nfunction useInView<T extends Element>() {\n  const ref = React.useRef<T>(null);\n  const [inView, setInView] = React.useState(false);\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    if (typeof IntersectionObserver === \"undefined\") return setInView(true);\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          setInView(true);\n          io.disconnect();\n        }\n      },\n      { threshold: 0.2 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n  return [ref, inView] as const;\n}\n\nexport function Barcode({ value, height = 44 }: { value: string; height?: number }) {\n  const bars = React.useMemo(() => {\n    let h = 2166136261;\n    const widths: number[] = [];\n    for (let i = 0; i < value.length * 5 + 24; i++) {\n      h ^= value.charCodeAt(i % value.length) + i;\n      h = Math.imul(h, 16777619) >>> 0;\n      widths.push((h % 3) + 1);\n    }\n    return widths;\n  }, [value]);\n\n  let x = 0;\n  const rects: React.ReactNode[] = [];\n  bars.forEach((w, i) => {\n    if (i % 2 === 0) rects.push(<rect key={i} x={x} y={0} width={w} height={height} />);\n    x += w;\n  });\n\n  return (\n    <svg\n      viewBox={`0 0 ${x} ${height}`}\n      preserveAspectRatio=\"none\"\n      width=\"100%\"\n      height={height}\n      fill=\"currentColor\"\n      aria-hidden\n    >\n      {rects}\n    </svg>\n  );\n}\n\nconst money = (n: number, c: string) => `${c}${n.toFixed(2)}`;\n\nexport function Receipt({\n  title = \"EPHEMERA GOODS CO.\",\n  subtitle,\n  meta,\n  items,\n  currency = \"$\",\n  taxRate = 0,\n  footer = \"THANK YOU, COME AGAIN\",\n  barcode = \"0000-EPHEMERA\",\n  print = true,\n  width = 300,\n  className = \"\",\n  style,\n  ...props\n}: ReceiptProps) {\n  const [ref, inView] = useInView<HTMLDivElement>();\n  const subtotal = items.reduce((s, i) => s + i.price * (i.qty ?? 1), 0);\n  const tax = subtotal * taxRate;\n  const lines = items.length + (meta?.length ?? 0) + 12;\n\n  const rule = <div className=\"my-2 border-t border-dashed border-current opacity-60\" />;\n\n  return (\n    <div\n      ref={ref}\n      className={`inline-block align-top ${className}`}\n      style={{ filter: \"drop-shadow(0 6px 10px rgb(0 0 0 / .14)) drop-shadow(0 1px 1px rgb(0 0 0 / .12))\", ...style }}\n      {...props}\n    >\n      <style href=\"eph-receipt\" precedence=\"default\">\n        {CSS}\n      </style>\n      <div\n        className={print ? (inView ? \"eph-receipt-printing\" : \"eph-receipt-hidden\") : undefined}\n        style={{\n          width,\n          maxWidth: \"100%\",\n          padding: \"28px 22px\",\n          color: \"#2e2d2a\",\n          background: `${NOISE}, linear-gradient(180deg, #fdfcf8, #f6f3ea)`,\n          mask: ZIGZAG,\n          WebkitMask: ZIGZAG,\n          fontFamily: \"var(--font-receipt, ui-monospace, 'SFMono-Regular', Menlo, monospace)\",\n          fontSize: 12.5,\n          lineHeight: 1.55,\n          letterSpacing: \"0.02em\",\n          textShadow: \"0 0 0.4px rgb(0 0 0 / .5)\",\n          animation: print && inView ? `eph-receipt-print ${Math.min(lines * 0.12, 3.2)}s steps(${lines * 2}) both` : undefined,\n        }}\n      >\n        <div className=\"text-center\">\n          <div className=\"text-[15px] font-bold tracking-[0.12em]\">{title}</div>\n          {subtitle && <div className=\"whitespace-pre-line opacity-80\">{subtitle}</div>}\n        </div>\n        {meta && meta.length > 0 && (\n          <>\n            {rule}\n            {meta.map((m) => (\n              <div key={m.label} className=\"flex justify-between gap-4\">\n                <span>{m.label}</span>\n                <span>{m.value}</span>\n              </div>\n            ))}\n          </>\n        )}\n        {rule}\n        {items.map((item, i) => (\n          <div key={i} className=\"flex justify-between gap-3\">\n            <span className=\"min-w-0\">\n              {(item.qty ?? 1) > 1 && <span className=\"opacity-70\">{item.qty} × </span>}\n              {item.name.toUpperCase()}\n            </span>\n            <span className=\"shrink-0 tabular-nums\">{money(item.price * (item.qty ?? 1), currency)}</span>\n          </div>\n        ))}\n        {rule}\n        {taxRate > 0 && (\n          <>\n            <div className=\"flex justify-between\">\n              <span>SUBTOTAL</span>\n              <span className=\"tabular-nums\">{money(subtotal, currency)}</span>\n            </div>\n            <div className=\"flex justify-between\">\n              <span>TAX {(taxRate * 100).toFixed(taxRate * 100 % 1 ? 1 : 0)}%</span>\n              <span className=\"tabular-nums\">{money(tax, currency)}</span>\n            </div>\n          </>\n        )}\n        <div className=\"mt-1 flex justify-between text-[16px] font-bold\">\n          <span>TOTAL</span>\n          <span className=\"tabular-nums\">{money(subtotal + tax, currency)}</span>\n        </div>\n        {rule}\n        {footer && <div className=\"mt-3 text-center tracking-[0.15em]\">{footer}</div>}\n        {barcode !== false && (\n          <div className=\"mt-4\">\n            <Barcode value={barcode} />\n            <div className=\"mt-1 text-center text-[10px] tracking-[0.3em] opacity-80\">{barcode}</div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "extras"
  ]
}