"use client"; import { CheckCircleIcon, CircleNotchIcon, } from "@phosphor-icons/react/dist/ssr"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type ComponentPropsWithoutRef, createContext, type ReactNode, useContext, } from "react"; import { cn } from "@/helpers/classname-helper"; export type ReceiptPrinterStage = "processing" | "printing" | "complete"; export type ReceiptFeedMotion = "smooth" | "stepped"; export type ReceiptPrinterRootProps = Omit< ComponentPropsWithoutRef<"section">, "children" > & { /** Disables all stage transitions when false. */ animate?: boolean; children: ReactNode; /** Controls whether the paper feeds continuously or one line at a time. */ feedMotion?: ReceiptFeedMotion; /** Current state of the printer. */ stage: ReceiptPrinterStage; }; export type ReceiptPrinterMachineProps = ComponentPropsWithoutRef<"div">; export type ReceiptPrinterHeaderProps = ComponentPropsWithoutRef<"div">; export type ReceiptPrinterScreenProps = ComponentPropsWithoutRef<"div">; export type ReceiptPrinterOutputProps = ComponentPropsWithoutRef<"div">; export type ReceiptPrinterPaperProps = ComponentPropsWithoutRef<"article">; export type ReceiptPrinterStatusProps = Omit< ComponentPropsWithoutRef<"div">, "children" > & { /** Custom status content. Defaults to a label derived from the current stage. */ children?: ReactNode; }; type ReceiptPrinterContextValue = { animate: boolean; feedMotion: ReceiptFeedMotion; shouldMove: boolean; stage: ReceiptPrinterStage; }; const ReceiptPrinterContext = createContext( null, ); const easeOut = [0.23, 1, 0.32, 1] as const; const easeInOut = [0.77, 0, 0.175, 1] as const; const receiptToothCount = 40; const receiptToothDepth = 4; const receiptToothPoints = Array.from( { length: receiptToothCount * 2 }, (_, index) => { const x = 100 - ((index + 1) * 100) / (receiptToothCount * 2); const y = index % 2 === 0 ? "100%" : `calc(100% - ${receiptToothDepth}px)`; return `${x}% ${y}`; }, ).join(", "); const receiptClipPath = `polygon(0 0, 100% 0, 100% calc(100% - ${receiptToothDepth}px), ${receiptToothPoints})`; const printingTransformKeyframes = [ "translateY(calc(-100% + 2px))", "translateY(-91%)", "translateY(-91%)", "translateY(-81%)", "translateY(-81%)", "translateY(-70%)", "translateY(-70%)", "translateY(-58%)", "translateY(-58%)", "translateY(-45%)", "translateY(-45%)", "translateY(-32%)", "translateY(-32%)", "translateY(-20%)", "translateY(-20%)", "translateY(-10%)", "translateY(-10%)", "translateY(-3%)", "translateY(-3%)", "translateY(0%)", ]; const printingKeyframeTimes = [ 0, 0.075, 0.105, 0.18, 0.21, 0.285, 0.315, 0.39, 0.42, 0.495, 0.525, 0.6, 0.63, 0.705, 0.735, 0.81, 0.84, 0.915, 0.945, 1, ]; const statusLabels: Record = { processing: "Processing your order", printing: "Printing your receipt", complete: "Order complete", }; const machineClassName = "relative isolate w-full overflow-hidden rounded-[var(--printer-radius)] border border-grayscale-12 bg-[color-mix(in_oklab,var(--color-grayscale-11)_30%,var(--color-grayscale-12))] p-[var(--printer-inset)] pb-8 shadow-[0_20px_36px_-20px_color-mix(in_oklab,var(--color-grayscale-12)_55%,transparent),0_6px_14px_-8px_color-mix(in_oklab,var(--color-grayscale-12)_24%,transparent),inset_0_1px_0_color-mix(in_oklab,var(--color-grayscale-1)_14%,transparent),inset_0_-1px_0_color-mix(in_oklab,var(--color-grayscale-12)_55%,transparent)] [--printer-inner-radius:calc(var(--printer-radius)_-_var(--printer-inset))] [--printer-inset:0.75rem] [--printer-radius:1.5rem] before:pointer-events-none before:absolute before:inset-0 before:z-0 before:rounded-[inherit] before:bg-[url('/textures/plastic-noise.svg')] before:bg-[length:180px_180px] before:bg-repeat before:opacity-30 before:mix-blend-multiply before:content-[''] dark:border-grayscale-3 dark:bg-grayscale-4 dark:shadow-[0_20px_36px_-20px_color-mix(in_oklab,var(--color-grayscale-3)_55%,transparent),0_6px_14px_-8px_color-mix(in_oklab,var(--color-grayscale-3)_24%,transparent),inset_0_1px_0_color-mix(in_oklab,var(--color-grayscale-12)_14%,transparent),inset_0_-1px_0_color-mix(in_oklab,var(--color-grayscale-3)_55%,transparent)]"; function useReceiptPrinter(component: string) { const context = useContext(ReceiptPrinterContext); if (!context) { throw new Error(`${component} must be used inside ReceiptPrinter.Root.`); } return context; } function ReceiptPrinterRoot({ "aria-label": ariaLabel = "Receipt printer", animate = true, children, className, feedMotion = "stepped", stage, ...props }: ReceiptPrinterRootProps) { const shouldReduceMotion = useReducedMotion(); const context = { animate, feedMotion, shouldMove: animate && !shouldReduceMotion, stage, }; return (
{children}
); } function ReceiptPrinterMachine({ children, className, ...props }: ReceiptPrinterMachineProps) { return (
{children} ); } function ReceiptPrinterHeader({ children, className, ...props }: ReceiptPrinterHeaderProps) { return (
{children}
); } function ReceiptPrinterScreen({ children, className, ...props }: ReceiptPrinterScreenProps) { return (
{children}
); } function StatusIndicator({ animate, move, stage, }: { animate: boolean; move: boolean; stage: ReceiptPrinterStage; }) { const isComplete = stage === "complete"; return ( ); } function ReceiptPrinterStatus({ children, className, ...props }: ReceiptPrinterStatusProps) { const { animate, shouldMove, stage } = useReceiptPrinter( "ReceiptPrinter.Status", ); return (
{children ?? statusLabels[stage]}
); } function ReceiptPrinterPaper({ children, className, style, ...props }: ReceiptPrinterPaperProps) { return (
{children}
); } function ReceiptPrinterOutput({ children, className, ...props }: ReceiptPrinterOutputProps) { const { animate, feedMotion, shouldMove, stage } = useReceiptPrinter( "ReceiptPrinter.Output", ); const isReceiptVisible = stage !== "processing"; const shouldUseSteppedFeed = feedMotion === "stepped" && stage === "printing" && shouldMove; return (
{isReceiptVisible ? ( ); } export const ReceiptPrinter = { Header: ReceiptPrinterHeader, Machine: ReceiptPrinterMachine, Output: ReceiptPrinterOutput, Paper: ReceiptPrinterPaper, Root: ReceiptPrinterRoot, Screen: ReceiptPrinterScreen, Status: ReceiptPrinterStatus, };