{ "name": "qr-code", "type": "registry:ui", "files": [ { "path": "ui/qr-code.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useLazyRef } from \"@/registry/bases/base/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"QRCode\";\nconst CANVAS_NAME = \"QRCodeCanvas\";\nconst SVG_NAME = \"QRCodeSvg\";\nconst IMAGE_NAME = \"QRCodeImage\";\nconst SKELETON_NAME = \"QRCodeSkeleton\";\n\ntype QRCodeLevel = \"L\" | \"M\" | \"Q\" | \"H\";\n\ninterface QRCodeCanvasOpts {\n errorCorrectionLevel: QRCodeLevel;\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n quality?: number;\n margin?: number;\n color?: {\n dark: string;\n light: string;\n };\n width?: number;\n rendererOpts?: {\n quality?: number;\n };\n}\n\ninterface StoreState {\n dataUrl: string | null;\n svgString: string | null;\n isGenerating: boolean;\n error: Error | null;\n generationKey: string;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n setState: (key: K, value: StoreState[K]) => void;\n setStates: (updates: Partial) => void;\n notify: () => void;\n}\n\ninterface QRCodeContextValue {\n value: string;\n size: number;\n margin: number;\n level: QRCodeLevel;\n backgroundColor: string;\n foregroundColor: string;\n canvasRef: React.RefObject;\n}\n\nconst StoreContext = React.createContext(null);\n\nfunction useStore(selector: (state: StoreState) => T): T {\n const store = React.useContext(StoreContext);\n if (!store) {\n throw new Error(`\\`useQRCode\\` must be used within \\`${ROOT_NAME}\\``);\n }\n\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\nconst QRCodeContext = React.createContext(null);\n\nfunction useQRCodeContext(consumerName: string) {\n const context = React.useContext(QRCodeContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface QRCodeProps extends Omit, \"onError\"> {\n value: string;\n size?: number;\n level?: QRCodeLevel;\n margin?: number;\n quality?: number;\n backgroundColor?: string;\n foregroundColor?: string;\n onError?: (error: Error) => void;\n onGenerated?: () => void;\n}\n\nfunction QRCode(props: QRCodeProps) {\n const {\n value,\n size = 200,\n level = \"M\",\n margin = 1,\n quality = 0.92,\n backgroundColor = \"#ffffff\",\n foregroundColor = \"#000000\",\n onError,\n onGenerated,\n className,\n style,\n render,\n ...rootProps\n } = props;\n\n const canvasRef = React.useRef(null);\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n const stateRef = useLazyRef(() => ({\n dataUrl: null,\n svgString: null,\n isGenerating: false,\n error: null,\n generationKey: \"\",\n }));\n\n const store = React.useMemo(() => {\n return {\n subscribe: (cb) => {\n listenersRef.current.add(cb);\n return () => listenersRef.current.delete(cb);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n stateRef.current[key] = value;\n store.notify();\n },\n setStates: (updates) => {\n let hasChanged = false;\n\n for (const key of Object.keys(updates) as Array) {\n const value = updates[key];\n if (value !== undefined && !Object.is(stateRef.current[key], value)) {\n Object.assign(stateRef.current, { [key]: value });\n hasChanged = true;\n }\n }\n\n if (hasChanged) {\n store.notify();\n }\n },\n notify: () => {\n for (const cb of listenersRef.current) {\n cb();\n }\n },\n };\n }, [listenersRef, stateRef]);\n\n const canvasOpts = React.useMemo(\n () => ({\n errorCorrectionLevel: level,\n type: \"image/png\",\n quality,\n margin,\n color: {\n dark: foregroundColor,\n light: backgroundColor,\n },\n width: size,\n }),\n [level, margin, foregroundColor, backgroundColor, size, quality],\n );\n\n const generationKey = React.useMemo(() => {\n if (!value) return \"\";\n\n return JSON.stringify({\n value,\n size,\n level,\n margin,\n quality,\n foregroundColor,\n backgroundColor,\n });\n }, [value, level, margin, foregroundColor, backgroundColor, size, quality]);\n\n const onQRCodeGenerate = React.useCallback(\n async (targetGenerationKey: string) => {\n if (!value || !targetGenerationKey) return;\n\n const currentState = store.getState();\n if (\n currentState.isGenerating ||\n currentState.generationKey === targetGenerationKey\n )\n return;\n\n store.setStates({\n isGenerating: true,\n error: null,\n });\n\n try {\n const QRCode = (await import(\"qrcode\")).default;\n\n let dataUrl: string | null = null;\n\n try {\n dataUrl = await QRCode.toDataURL(value, canvasOpts);\n } catch {\n dataUrl = null;\n }\n\n if (canvasRef.current) {\n await QRCode.toCanvas(canvasRef.current, value, canvasOpts);\n }\n\n const svgString = await QRCode.toString(value, {\n errorCorrectionLevel: canvasOpts.errorCorrectionLevel,\n margin: canvasOpts.margin,\n color: canvasOpts.color,\n width: canvasOpts.width,\n type: \"svg\",\n });\n\n store.setStates({\n dataUrl,\n svgString,\n isGenerating: false,\n generationKey: targetGenerationKey,\n });\n\n onGenerated?.();\n } catch (error) {\n const parsedError =\n error instanceof Error\n ? error\n : new Error(\"Failed to generate QR code\");\n store.setStates({\n error: parsedError,\n isGenerating: false,\n });\n onError?.(parsedError);\n }\n },\n [value, canvasOpts, store, onError, onGenerated],\n );\n\n const contextValue = React.useMemo(\n () => ({\n value,\n size,\n level,\n margin,\n backgroundColor,\n foregroundColor,\n canvasRef,\n }),\n [value, size, backgroundColor, foregroundColor, level, margin],\n );\n\n React.useLayoutEffect(() => {\n if (generationKey) {\n const rafId = requestAnimationFrame(() => {\n onQRCodeGenerate(generationKey);\n });\n\n return () => cancelAnimationFrame(rafId);\n }\n }, [generationKey, onQRCodeGenerate]);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\"relative flex flex-col items-center gap-2\", className),\n style: {\n \"--qr-code-size\": `${size}px`,\n ...style,\n } as React.CSSProperties,\n },\n rootProps,\n ),\n render,\n state: { slot: \"qr-code\" },\n });\n\n return (\n \n \n {element}\n \n \n );\n}\n\ninterface QRCodeCanvasProps\n extends React.ComponentProps<\"canvas\">,\n useRender.ComponentProps<\"canvas\"> {}\n\nfunction QRCodeCanvas({\n render,\n className,\n ref,\n ...canvasProps\n}: QRCodeCanvasProps) {\n const context = useQRCodeContext(CANVAS_NAME);\n const generationKey = useStore((state) => state.generationKey);\n\n const composedRef = useComposedRefs(ref, context.canvasRef);\n\n return useRender({\n defaultTagName: \"canvas\",\n props: mergeProps<\"canvas\">(\n {\n ref: composedRef,\n width: context.size,\n height: context.size,\n className: cn(\n \"relative max-h-(--qr-code-size) max-w-(--qr-code-size)\",\n !generationKey && \"invisible\",\n className,\n ),\n },\n canvasProps,\n ),\n render,\n state: { slot: \"qr-code-canvas\" },\n });\n}\n\ninterface QRCodeSvgProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction QRCodeSvg({ render, className, style, ...svgProps }: QRCodeSvgProps) {\n const context = useQRCodeContext(SVG_NAME);\n const svgString = useStore((state) => state.svgString);\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"relative max-h-(--qr-code-size) max-w-(--qr-code-size)\",\n className,\n ),\n style: { width: context.size, height: context.size, ...style },\n dangerouslySetInnerHTML: svgString ? { __html: svgString } : undefined,\n },\n svgProps,\n ),\n render,\n state: { slot: \"qr-code-svg\" },\n });\n\n if (!svgString) return null;\n\n return element;\n}\n\ninterface QRCodeImageProps\n extends React.ComponentProps<\"img\">,\n useRender.ComponentProps<\"img\"> {\n alt?: string;\n}\n\nfunction QRCodeImage({\n alt = \"QR Code\",\n render,\n className,\n ...imageProps\n}: QRCodeImageProps) {\n const context = useQRCodeContext(IMAGE_NAME);\n const dataUrl = useStore((state) => state.dataUrl);\n\n const element = useRender({\n defaultTagName: \"img\",\n props: mergeProps<\"img\">(\n {\n src: dataUrl ?? undefined,\n alt,\n width: context.size,\n height: context.size,\n className: cn(\n \"relative max-h-(--qr-code-size) max-w-(--qr-code-size)\",\n className,\n ),\n },\n imageProps,\n ),\n render,\n state: { slot: \"qr-code-image\" },\n });\n\n if (!dataUrl) return null;\n\n return element;\n}\n\ninterface QRCodeDownloadProps\n extends React.ComponentProps<\"button\">,\n useRender.ComponentProps<\"button\"> {\n filename?: string;\n format?: \"png\" | \"svg\";\n}\n\nfunction QRCodeDownload(props: QRCodeDownloadProps) {\n const {\n filename = \"qrcode\",\n format = \"png\",\n render,\n className,\n children,\n ...buttonProps\n } = props;\n\n const dataUrl = useStore((state) => state.dataUrl);\n const svgString = useStore((state) => state.svgString);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n buttonProps.onClick?.(event);\n if (event.defaultPrevented) return;\n\n const link = document.createElement(\"a\");\n\n if (format === \"png\" && dataUrl) {\n link.href = dataUrl;\n link.download = `${filename}.png`;\n } else if (format === \"svg\" && svgString) {\n const blob = new Blob([svgString], { type: \"image/svg+xml\" });\n link.href = URL.createObjectURL(blob);\n link.download = `${filename}.svg`;\n } else {\n return;\n }\n\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n\n if (format === \"svg\" && svgString) {\n URL.revokeObjectURL(link.href);\n }\n },\n [dataUrl, svgString, filename, format, buttonProps.onClick],\n );\n\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n type: \"button\",\n className: cn(\"max-w-(--qr-code-size)\", className),\n onClick,\n children: children ?? `Download ${format.toUpperCase()}`,\n },\n buttonProps,\n ),\n render,\n state: { slot: \"qr-code-download\" },\n });\n}\n\ninterface QRCodeOverlayProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction QRCodeOverlay({\n render,\n className,\n ...overlayProps\n}: QRCodeOverlayProps) {\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-sm bg-background\",\n className,\n ),\n },\n overlayProps,\n ),\n render,\n state: { slot: \"qr-code-overlay\" },\n });\n}\n\ninterface QRCodeSkeletonProps\n extends React.ComponentProps<\"div\">,\n useRender.ComponentProps<\"div\"> {}\n\nfunction QRCodeSkeleton({\n render,\n className,\n style,\n ...skeletonProps\n}: QRCodeSkeletonProps) {\n const context = useQRCodeContext(SKELETON_NAME);\n const dataUrl = useStore((state) => state.dataUrl);\n const svgString = useStore((state) => state.svgString);\n const generationKey = useStore((state) => state.generationKey);\n\n const isLoaded = dataUrl || svgString || generationKey;\n\n const element = useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"absolute max-h-(--qr-code-size) max-w-(--qr-code-size) animate-pulse bg-accent\",\n className,\n ),\n style: {\n width: context.size,\n height: context.size,\n ...style,\n },\n },\n skeletonProps,\n ),\n render,\n state: { slot: \"qr-code-skeleton\" },\n });\n\n if (isLoaded) return null;\n\n return element;\n}\n\nexport {\n QRCode,\n QRCodeCanvas,\n QRCodeDownload,\n QRCodeImage,\n QRCodeOverlay,\n type QRCodeProps,\n QRCodeSkeleton,\n QRCodeSvg,\n useStore as useQRCode,\n};\n", "target": "" }, { "path": "lib/compose-refs.ts", "type": "registry:lib", "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", "target": "" } ], "registryDependencies": [ "@diceui/use-lazy-ref" ], "dependencies": [ "@base-ui/react", "qrcode" ], "devDependencies": [ "@types/qrcode" ] }