{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "qr-code-scanner",
  "type": "registry:component",
  "title": "QR Code Scanner",
  "description": "Drop zone that decodes a QR code from an uploaded image in the browser, using the qrdx library.",
  "author": "bucharitesh <https://qrdx.dev>",
  "dependencies": ["qrdx", "lucide-react"],
  "registryDependencies": ["button"],
  "files": [
    {
      "path": "registry/qrdx/qr-code-scanner.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { Check, Copy, ExternalLink, ImageUp, Loader2 } from \"lucide-react\";\nimport { handleImageLoad } from \"qrdx\";\nimport {\n  type ChangeEvent,\n  type ComponentProps,\n  type DragEvent,\n  useId,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ntype ScanState =\n  | { status: \"idle\" }\n  | { status: \"scanning\" }\n  | { status: \"found\"; value: string }\n  | { status: \"not-found\" }\n  | { status: \"error\"; message: string };\n\nexport interface QrCodeScannerProps\n  extends Omit<ComponentProps<\"div\">, \"children\" | \"onDrop\"> {\n  /** Called with the decoded text when a QR code is found. */\n  onResult?: (value: string) => void;\n  /**\n   * Heading inside the drop zone.\n   * @default \"Scan a QR code from an image\"\n   */\n  label?: string;\n}\n\n/** Decodes the first QR code in an image file, in the browser. */\nasync function decodeImageFile(file: File): Promise<string | null> {\n  const url = URL.createObjectURL(file);\n  try {\n    const image = new Image();\n    image.src = url;\n    await image.decode();\n    const result = await handleImageLoad(image, image.naturalWidth);\n    return typeof result === \"string\" && result !== \"NO_QR\" ? result : null;\n  } finally {\n    URL.revokeObjectURL(url);\n  }\n}\n\nexport function QrCodeScanner({\n  onResult,\n  label = \"Scan a QR code from an image\",\n  className,\n  ...props\n}: QrCodeScannerProps) {\n  const inputId = useId();\n  const [state, setState] = useState<ScanState>({ status: \"idle\" });\n  const [dragging, setDragging] = useState(false);\n  const [copied, setCopied] = useState(false);\n\n  async function scan(file: File | undefined) {\n    if (!file) return;\n    if (!file.type.startsWith(\"image/\")) {\n      setState({ status: \"error\", message: \"Choose an image file.\" });\n      return;\n    }\n    setState({ status: \"scanning\" });\n    setCopied(false);\n    try {\n      const value = await decodeImageFile(file);\n      if (value === null) {\n        setState({ status: \"not-found\" });\n        return;\n      }\n      setState({ status: \"found\", value });\n      onResult?.(value);\n    } catch {\n      setState({ status: \"error\", message: \"Could not read that image.\" });\n    }\n  }\n\n  function onChange(event: ChangeEvent<HTMLInputElement>) {\n    scan(event.target.files?.[0]);\n    event.target.value = \"\";\n  }\n\n  function onDrop(event: DragEvent<HTMLLabelElement>) {\n    event.preventDefault();\n    setDragging(false);\n    scan(event.dataTransfer.files[0]);\n  }\n\n  async function copy(value: string) {\n    await navigator.clipboard.writeText(value);\n    setCopied(true);\n  }\n\n  return (\n    <div\n      className={cn(\"flex w-full max-w-sm flex-col gap-3\", className)}\n      data-slot=\"qr-code-scanner\"\n      {...props}\n    >\n      <label\n        className={cn(\n          \"flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-6 py-8 text-center text-sm transition-colors focus-within:ring-[3px] focus-within:ring-ring/50 hover:bg-accent/50\",\n          dragging && \"border-primary bg-accent/50\",\n        )}\n        htmlFor={inputId}\n        onDragLeave={() => setDragging(false)}\n        onDragOver={(event) => {\n          event.preventDefault();\n          setDragging(true);\n        }}\n        onDrop={onDrop}\n      >\n        <span className=\"flex size-10 items-center justify-center rounded-full border bg-background\">\n          {state.status === \"scanning\" ? (\n            <Loader2\n              aria-hidden=\"true\"\n              className=\"size-4 animate-spin text-muted-foreground\"\n            />\n          ) : (\n            <ImageUp\n              aria-hidden=\"true\"\n              className=\"size-4 text-muted-foreground\"\n            />\n          )}\n        </span>\n        <span className=\"font-medium\">{label}</span>\n        <span className=\"text-muted-foreground text-xs\">\n          Drop an image here or click to choose one. It never leaves your\n          device.\n        </span>\n        <input\n          accept=\"image/*\"\n          className=\"sr-only\"\n          id={inputId}\n          onChange={onChange}\n          type=\"file\"\n        />\n      </label>\n\n      <div aria-live=\"polite\" className=\"min-h-5 text-sm\">\n        {state.status === \"scanning\" ? (\n          <p className=\"text-muted-foreground\">Scanning…</p>\n        ) : null}\n        {state.status === \"not-found\" ? (\n          <p className=\"text-muted-foreground\">\n            No QR code found. Try a sharper or closer image.\n          </p>\n        ) : null}\n        {state.status === \"error\" ? (\n          <p className=\"text-destructive\">{state.message}</p>\n        ) : null}\n        {state.status === \"found\" ? (\n          <div className=\"flex flex-col gap-3 rounded-lg border p-3\">\n            <code className=\"break-all font-mono text-xs\">{state.value}</code>\n            <div className=\"flex flex-wrap gap-2\">\n              <Button\n                onClick={() => copy(state.value)}\n                size=\"sm\"\n                type=\"button\"\n                variant=\"outline\"\n              >\n                {copied ? (\n                  <Check aria-hidden=\"true\" />\n                ) : (\n                  <Copy aria-hidden=\"true\" />\n                )}\n                {copied ? \"Copied\" : \"Copy\"}\n              </Button>\n              {/^https?:\\/\\//i.test(state.value) ? (\n                <Button asChild size=\"sm\" variant=\"ghost\">\n                  <a\n                    href={state.value}\n                    rel=\"noopener noreferrer nofollow\"\n                    target=\"_blank\"\n                  >\n                    <ExternalLink aria-hidden=\"true\" />\n                    Open link\n                  </a>\n                </Button>\n              ) : null}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "docs": "Import it with `import { QrCodeScanner } from \"@/components/qr-code-scanner\"`. Props and examples: https://qrdx.dev/docs/components/qr-code-scanner",
  "categories": ["qr-code"]
}
