{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "receipt-printer",
  "type": "registry:ui",
  "title": "Receipt Printer",
  "description": "A checkout state that turns payment processing into a printed order receipt, with a stepped paper feed, a status screen and a serrated paper tear.",
  "dependencies": ["@vueuse/core", "motion-v"],
  "files": [
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinter.vue",
      "content": "<script setup lang=\"ts\">\n  import { computed, provide } from 'vue';\n  import { usePreferredReducedMotion } from '@vueuse/core';\n  import type { ReceiptFeedMotion, ReceiptPrinterStage } from './types';\n  import { RECEIPT_PRINTER_CONTEXT_KEY } from './receipt-printer-context';\n  import { cn } from '~/lib/utils';\n\n  const props = withDefaults(\n    defineProps<{\n      /** Current state of the printer. */\n      stage: ReceiptPrinterStage;\n      /** Disables all stage transitions when false. */\n      animate?: boolean;\n      /** Controls whether the paper feeds continuously or one line at a time. */\n      feedMotion?: ReceiptFeedMotion;\n      ariaLabel?: string;\n      class?: string;\n    }>(),\n    {\n      animate: true,\n      feedMotion: 'stepped',\n      ariaLabel: 'Receipt printer',\n      class: '',\n    },\n  );\n\n  const shouldReduceMotion = usePreferredReducedMotion();\n\n  const context = computed(() => ({\n    animate: props.animate,\n    feedMotion: props.feedMotion,\n    shouldMove: props.animate && shouldReduceMotion.value !== 'reduce',\n    stage: props.stage,\n  }));\n\n  provide(RECEIPT_PRINTER_CONTEXT_KEY, context);\n</script>\n\n<template>\n  <section\n    :aria-label=\"ariaLabel\"\n    :data-stage=\"stage\"\n    :class=\"\n      cn(\n        'relative isolate flex w-full max-w-sm flex-col items-center',\n        props.class,\n      )\n    \"\n  >\n    <slot />\n  </section>\n</template>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinter.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterHeader.vue",
      "content": "<script setup lang=\"ts\">\n  import { cn } from '~/lib/utils';\n\n  defineProps<{\n    class?: string;\n  }>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn('relative z-10 flex h-11 items-start justify-between', $props.class)\n    \"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterHeader.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterMachine.vue",
      "content": "<script setup lang=\"ts\">\n  import { cn } from '~/lib/utils';\n\n  defineProps<{\n    class?: string;\n  }>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'machine-shell relative isolate w-full overflow-hidden rounded-(--printer-radius) border border-zinc-900 bg-zinc-800 p-(--printer-inset) pb-8 shadow-[0_20px_36px_-20px_rgba(24,24,27,0.55),0_6px_14px_-8px_rgba(24,24,27,0.24),inset_0_1px_0_rgba(250,250,250,0.14),inset_0_-1px_0_rgba(24,24,27,0.55)] [--printer-inner-radius:calc(var(--printer-radius)-var(--printer-inset))] [--printer-inset:0.75rem] [--printer-radius:1.5rem] dark:border-zinc-300 dark:bg-zinc-200 dark:shadow-[0_20px_36px_-20px_rgba(24,24,27,0.55),0_6px_14px_-8px_rgba(24,24,27,0.24),inset_0_1px_0_rgba(250,250,250,0.14),inset_0_-1px_0_rgba(24,24,27,0.55)]',\n        $props.class,\n      )\n    \"\n  >\n    <slot />\n    <div\n      aria-hidden=\"true\"\n      class=\"absolute inset-x-6 bottom-(--printer-inset) z-40 h-2 rounded-sm border border-zinc-900 bg-zinc-900 shadow-inner shadow-zinc-900 dark:border-zinc-50 dark:bg-zinc-50 dark:shadow-zinc-50\"\n    />\n  </div>\n</template>\n\n<style scoped>\n  .machine-shell::before {\n    content: '';\n    pointer-events: none;\n    position: absolute;\n    inset: 0;\n    z-index: 0;\n    border-radius: inherit;\n    background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22180%22%20height%3D%22180%22%20viewBox%3D%220%200%20180%20180%22%3E%3Cfilter%20id%3D%22noise%22%3E%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.92%22%20numOctaves%3D%223%22%20stitchTiles%3D%22stitch%22%20%2F%3E%3CfeColorMatrix%20type%3D%22saturate%22%20values%3D%220%22%20%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20filter%3D%22url%28%23noise%29%22%20%2F%3E%3C%2Fsvg%3E');\n    background-size: 180px 180px;\n    background-repeat: repeat;\n    opacity: 0.3;\n    mix-blend-mode: multiply;\n  }\n</style>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterMachine.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterOutput.vue",
      "content": "<script setup lang=\"ts\">\n  import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue';\n  import { motion } from 'motion-v';\n  import { RECEIPT_PRINTER_CONTEXT_KEY } from './receipt-printer-context';\n  import { cn } from '~/lib/utils';\n\n  const props = defineProps<{\n    class?: string;\n  }>();\n\n  const context = inject(RECEIPT_PRINTER_CONTEXT_KEY);\n  if (!context) {\n    throw new Error(\n      'ReceiptPrinter.Output must be used inside ReceiptPrinter.',\n    );\n  }\n\n  const EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\n  /**\n   * Stepped paper feed — the receipt creeps down one line at a time. Each\n   * translate is held briefly (the duplicated keyframes), matching the\n   * mechanical ratchet of a receipt printer.\n   */\n  const PRINTING_Y = [\n    '-100%',\n    '-91%',\n    '-91%',\n    '-81%',\n    '-81%',\n    '-70%',\n    '-70%',\n    '-58%',\n    '-58%',\n    '-45%',\n    '-45%',\n    '-32%',\n    '-32%',\n    '-20%',\n    '-20%',\n    '-10%',\n    '-10%',\n    '-3%',\n    '-3%',\n    '0%',\n  ];\n  const PRINTING_TIMES = [\n    0, 0.075, 0.105, 0.18, 0.21, 0.285, 0.315, 0.39, 0.42, 0.495, 0.525, 0.6,\n    0.63, 0.705, 0.735, 0.81, 0.84, 0.915, 0.945, 1,\n  ];\n\n  const wrapRef = ref<HTMLElement | null>(null);\n  let feedFrame: number | null = null;\n  let feedStart = 0;\n  let feedSmooth = false;\n\n  const isReceiptVisible = computed(() => context.value.stage !== 'processing');\n\n  /** translateY between two keyframes at progress p (0..1). */\n  function yAt(p: number): string {\n    const times = PRINTING_TIMES;\n    if (p <= times[0]!) return PRINTING_Y[0]!;\n    if (p >= times[times.length - 1]!) {\n      return PRINTING_Y[PRINTING_Y.length - 1]!;\n    }\n    let i = 1;\n    while (times[i]! < p) i += 1;\n    const t0 = times[i - 1]!;\n    const t1 = times[i]!;\n    const y0 = Number.parseFloat(PRINTING_Y[i - 1]!);\n    const y1 = Number.parseFloat(PRINTING_Y[i]!);\n    const k = t1 === t0 ? 1 : (p - t0) / (t1 - t0);\n    return `${(y0 + (y1 - y0) * k).toFixed(2)}%`;\n  }\n\n  function setFeedY(y: string): void {\n    const sheet = wrapRef.value?.querySelector<HTMLElement>('.receipt-sheet');\n    if (sheet) {\n      sheet.style.translate = `0 ${y}`;\n    }\n  }\n\n  function stopFeed(): void {\n    if (feedFrame !== null) {\n      cancelAnimationFrame(feedFrame);\n      feedFrame = null;\n    }\n  }\n\n  function runFeed(): void {\n    stopFeed();\n    if (!wrapRef.value) return;\n    const duration = context.value.shouldMove ? 1750 : 0;\n    feedSmooth = context.value.feedMotion === 'smooth';\n    if (duration === 0) {\n      setFeedY('0%');\n      return;\n    }\n    feedStart = performance.now();\n    const tick = (now: number): void => {\n      const p = Math.min(1, (now - feedStart) / duration);\n      setFeedY(feedSmooth ? `${(-100 + p * 100).toFixed(2)}%` : yAt(p));\n      if (p < 1) {\n        feedFrame = requestAnimationFrame(tick);\n      } else {\n        feedFrame = null;\n      }\n    };\n    feedFrame = requestAnimationFrame(tick);\n  }\n\n  watch(\n    () => context.value.stage,\n    (stage) => {\n      if (stage === 'printing' && context.value.shouldMove) {\n        runFeed();\n      } else if (stage === 'complete' || !context.value.shouldMove) {\n        stopFeed();\n        setFeedY('0%');\n      } else {\n        stopFeed();\n        setFeedY('-100%');\n      }\n    },\n    { immediate: true },\n  );\n\n  onMounted(() => {\n    // the receipt starts hidden above the slot\n    setFeedY('-100%');\n  });\n\n  onUnmounted(stopFeed);\n</script>\n\n<template>\n  <div\n    ref=\"wrapRef\"\n    :class=\"\n      cn(\n        'relative z-50 -mt-4 h-128 w-[calc(80%+3rem)] max-w-full overflow-hidden px-6',\n        props.class,\n      )\n    \"\n  >\n    <div\n      v-if=\"isReceiptVisible\"\n      aria-hidden=\"true\"\n      class=\"pointer-events-none absolute inset-x-6 -top-1 z-20 h-2 bg-zinc-900/75 blur-sm dark:bg-zinc-50/75\"\n    />\n\n    <component\n      :is=\"motion.div\"\n      :animate=\"{ opacity: isReceiptVisible ? 1 : 0 }\"\n      :aria-hidden=\"context.stage !== 'complete'\"\n      :initial=\"false\"\n      :transition=\"{\n        opacity: { duration: context.animate ? 0.16 : 0, ease: EASE_OUT },\n      }\"\n      class=\"receipt-sheet relative isolate before:pointer-events-none before:absolute before:inset-x-3 before:top-3 before:bottom-4 before:z-0 before:rounded-sm before:shadow-[0_8px_24px_rgba(24,24,27,0.24)] before:content-[''] after:pointer-events-none after:absolute after:inset-x-[8%] after:bottom-0 after:z-0 after:h-3 after:translate-y-1.5 after:rounded-full after:bg-zinc-900/10 after:blur-lg after:content-[''] dark:before:shadow-[0_8px_24px_rgba(250,250,250,0.2)] dark:after:bg-zinc-50/10\"\n    >\n      <slot />\n    </component>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterOutput.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterPaper.vue",
      "content": "<script setup lang=\"ts\">\n  import { cn } from '~/lib/utils';\n\n  defineProps<{\n    class?: string;\n  }>();\n\n  const RECEIPT_TOOTH_COUNT = 40;\n  const RECEIPT_TOOTH_DEPTH = 4;\n\n  const receiptToothPoints = Array.from(\n    { length: RECEIPT_TOOTH_COUNT * 2 },\n    (_, index) => {\n      const x = 100 - ((index + 1) * 100) / (RECEIPT_TOOTH_COUNT * 2);\n      const y =\n        index % 2 === 0 ? '100%' : `calc(100% - ${RECEIPT_TOOTH_DEPTH}px)`;\n      return `${x}% ${y}`;\n    },\n  ).join(', ');\n\n  const receiptClipPath = `polygon(0 0, 100% 0, 100% calc(100% - ${RECEIPT_TOOTH_DEPTH}px), ${receiptToothPoints})`;\n</script>\n\n<template>\n  <article\n    :style=\"{ clipPath: receiptClipPath }\"\n    :class=\"\n      cn(\n        'paper-surface relative z-10 min-h-80 bg-zinc-50 px-6 pt-7 pb-8 font-mono text-zinc-900 dark:bg-zinc-900 dark:text-zinc-50',\n        $props.class,\n      )\n    \"\n  >\n    <slot />\n  </article>\n</template>\n\n<style scoped>\n  .paper-surface {\n    background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22360%22%20height%3D%22520%22%20viewBox%3D%220%200%20360%20520%22%3E%3Cdefs%3E%3Cfilter%20id%3D%22paper%22%20x%3D%22-10%25%22%20y%3D%22-10%25%22%20width%3D%22120%25%22%20height%3D%22120%25%22%20color-interpolation-filters%3D%22sRGB%22%3E%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.008%200.016%22%20numOctaves%3D%224%22%20seed%3D%2217%22%20stitchTiles%3D%22stitch%22%20result%3D%22coarse%22%20%2F%3E%3CfeDiffuseLighting%20in%3D%22coarse%22%20surfaceScale%3D%223.2%22%20diffuseConstant%3D%220.72%22%20lighting-color%3D%22%23ffffff%22%20result%3D%22coarse-light%22%3E%3CfeDistantLight%20azimuth%3D%22225%22%20elevation%3D%2255%22%20%2F%3E%3C%2FfeDiffuseLighting%3E%3CfeTurbulence%20type%3D%22fractalNoise%22%20baseFrequency%3D%220.09%200.12%22%20numOctaves%3D%222%22%20seed%3D%2231%22%20stitchTiles%3D%22stitch%22%20result%3D%22fine%22%20%2F%3E%3CfeDiffuseLighting%20in%3D%22fine%22%20surfaceScale%3D%220.55%22%20diffuseConstant%3D%220.55%22%20lighting-color%3D%22%23ffffff%22%20result%3D%22fine-light%22%3E%3CfeDistantLight%20azimuth%3D%22205%22%20elevation%3D%2265%22%20%2F%3E%3C%2FfeDiffuseLighting%3E%3CfeBlend%20in%3D%22coarse-light%22%20in2%3D%22fine-light%22%20mode%3D%22multiply%22%20result%3D%22combined%22%20%2F%3E%3CfeComponentTransfer%20in%3D%22combined%22%3E%3CfeFuncR%20type%3D%22linear%22%20slope%3D%220.42%22%20intercept%3D%220.29%22%20%2F%3E%3CfeFuncG%20type%3D%22linear%22%20slope%3D%220.42%22%20intercept%3D%220.29%22%20%2F%3E%3CfeFuncB%20type%3D%22linear%22%20slope%3D%220.42%22%20intercept%3D%220.29%22%20%2F%3E%3C%2FfeComponentTransfer%3E%3C%2Ffilter%3E%3C%2Fdefs%3E%3Crect%20width%3D%22360%22%20height%3D%22520%22%20fill%3D%22%23808080%22%20filter%3D%22url%28%23paper%29%22%20%2F%3E%3C%2Fsvg%3E');\n    background-size: cover;\n    background-blend-mode: soft-light;\n  }\n</style>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterPaper.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterScreen.vue",
      "content": "<script setup lang=\"ts\">\n  import { cn } from '~/lib/utils';\n\n  defineProps<{\n    class?: string;\n  }>();\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'relative z-10 isolate overflow-hidden rounded-(--printer-inner-radius) border border-zinc-900 bg-zinc-900 p-4 text-zinc-50 shadow-inner shadow-zinc-900/80 after:pointer-events-none after:absolute after:inset-0 after:z-20 after:rounded-[inherit] after:shadow-[inset_0_0_24px_4px_rgba(24,24,27,0.52)] after:content-[\\'\\'] dark:border-zinc-200 dark:bg-zinc-200 dark:text-zinc-900 dark:shadow-zinc-200/80 dark:after:shadow-[inset_0_0_24px_4px_rgba(250,250,250,0.52)]',\n        $props.class,\n      )\n    \"\n  >\n    <div class=\"relative z-10\">\n      <slot />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterScreen.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/ReceiptPrinterStatus.vue",
      "content": "<script setup lang=\"ts\">\n  import { computed, inject } from 'vue';\n  import { AnimatePresence, motion } from 'motion-v';\n  import { RECEIPT_PRINTER_CONTEXT_KEY } from './receipt-printer-context';\n  import type { ReceiptPrinterStage } from './types';\n  import { cn } from '~/lib/utils';\n\n  const props = defineProps<{\n    /** Custom status content. Defaults to a label derived from the current stage. */\n    label?: string;\n    class?: string;\n  }>();\n\n  const context = inject(RECEIPT_PRINTER_CONTEXT_KEY);\n  if (!context) {\n    throw new Error(\n      'ReceiptPrinter.Status must be used inside ReceiptPrinter.',\n    );\n  }\n\n  const EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\n  const STATUS_LABELS: Record<ReceiptPrinterStage, string> = {\n    processing: 'Processing your order',\n    printing: 'Printing your receipt',\n    complete: 'Order complete',\n  };\n\n  const isComplete = computed(() => context.value.stage === 'complete');\n</script>\n\n<template>\n  <div :class=\"cn('flex min-w-0 items-center gap-2', props.class)\">\n    <span\n      aria-hidden=\"true\"\n      class=\"relative grid size-5 shrink-0 place-items-center\"\n    >\n      <AnimatePresence :initial=\"false\" mode=\"sync\">\n        <component\n          :is=\"motion.span\"\n          v-if=\"isComplete\"\n          key=\"complete\"\n          :initial=\"{\n            opacity: context.animate ? 0 : 1,\n            scale: context.shouldMove ? 0.94 : 1,\n          }\"\n          :animate=\"{ opacity: 1, scale: 1 }\"\n          :exit=\"{\n            opacity: context.animate ? 0 : 1,\n            scale: context.shouldMove ? 0.96 : 1,\n          }\"\n          :transition=\"{ duration: context.animate ? 0.16 : 0, ease: EASE_OUT }\"\n          class=\"col-start-1 row-start-1 grid place-items-center text-green-600 dark:text-green-400\"\n        >\n          <Icon name=\"lucide:circle-check\" class=\"size-4.5\" />\n        </component>\n        <component\n          :is=\"motion.span\"\n          v-else\n          key=\"working\"\n          :initial=\"{\n            opacity: context.animate ? 0 : 1,\n            scale: context.shouldMove ? 0.94 : 1,\n          }\"\n          :animate=\"{ opacity: 1, scale: 1 }\"\n          :exit=\"{\n            opacity: context.animate ? 0 : 1,\n            scale: context.shouldMove ? 0.96 : 1,\n          }\"\n          :transition=\"{ duration: context.animate ? 0.16 : 0, ease: EASE_OUT }\"\n          class=\"col-start-1 row-start-1 grid place-items-center text-zinc-500 dark:text-zinc-400\"\n        >\n          <Icon\n            name=\"lucide:loader-circle\"\n            :class=\"\n              context.animate ? 'animate-spin motion-reduce:animate-none' : ''\n            \"\n            class=\"size-4.5\"\n          />\n        </component>\n      </AnimatePresence>\n    </span>\n\n    <div\n      aria-live=\"polite\"\n      role=\"status\"\n      class=\"grid min-w-0 flex-1 items-center\"\n    >\n      <AnimatePresence :initial=\"false\" mode=\"sync\">\n        <component\n          :is=\"motion.div\"\n          :key=\"context.stage\"\n          :initial=\"{\n            opacity: context.animate ? 0 : 1,\n            y: context.shouldMove ? 4 : 0,\n          }\"\n          :animate=\"{ opacity: 1, y: 0 }\"\n          :exit=\"{\n            opacity: context.animate ? 0 : 1,\n            y: context.shouldMove ? -4 : 0,\n          }\"\n          :transition=\"{ duration: context.animate ? 0.18 : 0, ease: EASE_OUT }\"\n          class=\"col-start-1 row-start-1 truncate text-xs leading-none font-medium text-zinc-500 dark:text-zinc-400\"\n        >\n          {{ props.label ?? STATUS_LABELS[context.stage] }}\n        </component>\n      </AnimatePresence>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/ReceiptPrinterStatus.vue"
    },
    {
      "path": "registry/new-york/receipt-printer/index.ts",
      "content": "export { default as ReceiptPrinter } from './ReceiptPrinter.vue';\nexport { default as ReceiptPrinterMachine } from './ReceiptPrinterMachine.vue';\nexport { default as ReceiptPrinterHeader } from './ReceiptPrinterHeader.vue';\nexport { default as ReceiptPrinterScreen } from './ReceiptPrinterScreen.vue';\nexport { default as ReceiptPrinterStatus } from './ReceiptPrinterStatus.vue';\nexport { default as ReceiptPrinterPaper } from './ReceiptPrinterPaper.vue';\nexport { default as ReceiptPrinterOutput } from './ReceiptPrinterOutput.vue';\nexport type {\n  ReceiptPrinterStage,\n  ReceiptFeedMotion,\n  ReceiptPrinterContext,\n} from './types';\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/index.ts"
    },
    {
      "path": "registry/new-york/receipt-printer/receipt-printer-context.ts",
      "content": "import type { ComputedRef, InjectionKey } from 'vue';\nimport type { ReceiptPrinterContext } from './types';\n\nexport const RECEIPT_PRINTER_CONTEXT_KEY: InjectionKey<\n  ComputedRef<ReceiptPrinterContext>\n> = Symbol('nxui-receipt-printer');\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/receipt-printer-context.ts"
    },
    {
      "path": "registry/new-york/receipt-printer/types.ts",
      "content": "export type ReceiptPrinterStage = 'processing' | 'printing' | 'complete';\n\nexport type ReceiptFeedMotion = 'smooth' | 'stepped';\n\nexport interface ReceiptPrinterContext {\n  /** Stage transitions animate only when true. */\n  animate: boolean;\n  /** Whether the paper feeds continuously or one line at a time. */\n  feedMotion: ReceiptFeedMotion;\n  /** True when motion is enabled and the viewer has not requested reduced motion. */\n  shouldMove: boolean;\n  /** Current state of the printer. */\n  stage: ReceiptPrinterStage;\n}\n",
      "type": "registry:ui",
      "target": "components/ui/receipt-printer/types.ts"
    }
  ]
}
