{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "qr-code",
  "title": "QR Code",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A QR code drawn as accessible SVG: square, rounded or dotted modules that keep scannable corner markers, a logo slot, error-correction levels, a clear message when text is too long, and PNG and SVG downloads.",
  "dependencies": [
    "uqr@^0.1"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/qr-code.tsx",
      "content": "// Ballmac UI: QR Code. https://ui.ballmac.com/components/qr-code\n// Based on uqr (MIT, Copyright (c) 2023 Anthony Fu), which makes the code; this draws it as accessible SVG with shapes, a logo slot and downloads.\n\"use client\"\n\nimport * as React from \"react\"\nimport { encode, QrCodeDataType } from \"uqr\"\n\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { cn } from \"@/lib/utils\"\n\ntype QRCodeShape = \"square\" | \"rounded\" | \"dots\"\ntype QRCodeLevel = \"L\" | \"M\" | \"Q\" | \"H\"\n\ntype QRCodeProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** What the code holds: a web address, text, a Wi-Fi string or anything else. */\n  value: string\n  /** Pixel size of the whole code, quiet zone included. */\n  size?: number\n  /** How much damage it survives: L 7%, M 15%, Q 25%, H 30%. More makes a denser code. A logo raises it to at least Q. */\n  level?: QRCodeLevel\n  /** Shape of the dark modules. The three corner squares keep their own style so scanners find them. */\n  shape?: QRCodeShape\n  /** Empty modules around the code. Scanners need at least 4. */\n  quietZone?: number\n  /** Dark color. Codes scan best dark on light, so the defaults stay that way in dark mode. Any CSS color. */\n  foreground?: string\n  /** Light color. */\n  background?: string\n  /** Something drawn in the middle, such as a logo. Its area is cleared. */\n  logo?: React.ReactNode\n  /** Share of the width the logo takes, up to 0.3. */\n  logoSize?: number\n  /** Names the code for screen readers. Defaults to \"QR code for {value}\". */\n  label?: string\n  /** Called when the value cannot be encoded. */\n  onError?: (error: Error) => void\n  /** The SVG element, for downloads. */\n  svgRef?: React.Ref<SVGSVGElement>\n}\n\ntype Matrix = { size: number; data: boolean[][]; types: QrCodeDataType[][] }\n\n/** Make the code, or say why it cannot be made. Longer text than version 40 holds is the usual reason. */\nexport function makeMatrix(value: string, level: QRCodeLevel): { matrix: Matrix } | { error: Error } {\n  try {\n    const r = encode(value, { ecc: level, border: 0, boostEcc: false })\n    return { matrix: { size: r.size, data: r.data, types: r.types as QrCodeDataType[][] } }\n  } catch (e) {\n    return { error: e instanceof Error ? e : new Error(String(e)) }\n  }\n}\n\nconst n = (v: number) => Math.round(v * 1000) / 1000\n\n/** Path data for the dark modules, merging runs along a row, so a big code stays a small file. */\nfunction squarePath(m: Matrix, skip: (x: number, y: number) => boolean) {\n  let d = \"\"\n  for (let y = 0; y < m.size; y++) {\n    let x = 0\n    while (x < m.size) {\n      if (!m.data[y]![x] || skip(x, y)) {\n        x++\n        continue\n      }\n      let run = 1\n      while (x + run < m.size && m.data[y]![x + run] && !skip(x + run, y)) run++\n      d += `M${x} ${y}h${run}v1h${-run}z`\n      x += run\n    }\n  }\n  return d\n}\n\nfunction roundedPath(m: Matrix, skip: (x: number, y: number) => boolean, r = 0.32) {\n  let d = \"\"\n  const on = (x: number, y: number) => x >= 0 && y >= 0 && x < m.size && y < m.size && m.data[y]![x] && !skip(x, y)\n  for (let y = 0; y < m.size; y++)\n    for (let x = 0; x < m.size; x++) {\n      if (!on(x, y)) continue\n      // a corner is rounded only where no neighbour touches it, so joined modules read as one shape\n      const tl = !on(x - 1, y) && !on(x, y - 1) ? r : 0\n      const tr = !on(x + 1, y) && !on(x, y - 1) ? r : 0\n      const br = !on(x + 1, y) && !on(x, y + 1) ? r : 0\n      const bl = !on(x - 1, y) && !on(x, y + 1) ? r : 0\n      d += `M${n(x + tl)} ${y}H${n(x + 1 - tr)}${tr ? `A${r} ${r} 0 0 1 ${x + 1} ${n(y + tr)}` : \"\"}V${n(y + 1 - br)}${br ? `A${r} ${r} 0 0 1 ${n(x + 1 - br)} ${y + 1}` : \"\"}H${n(x + bl)}${bl ? `A${r} ${r} 0 0 1 ${x} ${n(y + 1 - bl)}` : \"\"}V${n(y + tl)}${tl ? `A${r} ${r} 0 0 1 ${n(x + tl)} ${y}` : \"\"}z`\n    }\n  return d\n}\n\nfunction dotsPath(m: Matrix, skip: (x: number, y: number) => boolean) {\n  let d = \"\"\n  const r = 0.44\n  for (let y = 0; y < m.size; y++)\n    for (let x = 0; x < m.size; x++) if (m.data[y]![x] && !skip(x, y)) d += `M${n(x + 0.5 - r)} ${n(y + 0.5)}a${r} ${r} 0 1 0 ${n(2 * r)} 0a${r} ${r} 0 1 0 ${n(-2 * r)} 0z`\n  return d\n}\n\n/** One of the three corner markers: a ring of seven, a gap, and a block of three. Drawn with even-odd fill so the gap is open. */\nfunction finder(x: number, y: number, shape: QRCodeShape) {\n  if (shape === \"square\") return `M${x} ${y}h7v7h-7zM${x + 1} ${y + 1}v5h5v-5zM${x + 2} ${y + 2}h3v3h-3z`\n  const ring = (px: number, py: number, s: number, r: number) =>\n    `M${px + r} ${py}h${s - 2 * r}a${r} ${r} 0 0 1 ${r} ${r}v${s - 2 * r}a${r} ${r} 0 0 1 ${-r} ${r}h${-(s - 2 * r)}a${r} ${r} 0 0 1 ${-r} ${-r}v${-(s - 2 * r)}a${r} ${r} 0 0 1 ${r} ${-r}z`\n  const hole = (px: number, py: number, s: number, r: number) =>\n    `M${px + r} ${py}a${r} ${r} 0 0 0 ${-r} ${r}v${s - 2 * r}a${r} ${r} 0 0 0 ${r} ${r}h${s - 2 * r}a${r} ${r} 0 0 0 ${r} ${-r}v${-(s - 2 * r)}a${r} ${r} 0 0 0 ${-r} ${-r}z`\n  const outer = 1.6\n  const inner = 1.1\n  return `${ring(x, y, 7, outer)}${hole(x + 1, y + 1, 5, outer - 0.6)}${ring(x + 2, y + 2, 3, inner)}`\n}\n\n/**\n * A QR code as SVG: it scales without blurring, has a name for screen readers, and can hold a logo. The shapes keep the three corner markers\n * recognizable, so rounded and dotted codes still scan. A value that is too long is reported, not drawn wrong.\n */\nfunction QRCode({\n  value,\n  size = 192,\n  level = \"M\",\n  shape = \"square\",\n  quietZone = 4,\n  foreground = \"rgb(0, 0, 0)\",\n  background = \"rgb(255, 255, 255)\",\n  logo,\n  logoSize = 0.22,\n  label,\n  onError,\n  svgRef,\n  className,\n  style,\n  ...props\n}: QRCodeProps) {\n  const msg = useMessages()\n  const effective: QRCodeLevel = logo && (level === \"L\" || level === \"M\") ? \"Q\" : level\n  const made = React.useMemo(() => (value ? makeMatrix(value, effective) : null), [value, effective])\n  const failed = made && \"error\" in made ? made.error : null\n  const onErrorRef = React.useRef(onError)\n  onErrorRef.current = onError\n  React.useEffect(() => {\n    if (failed) onErrorRef.current?.(failed)\n  }, [failed])\n\n  const clamp = Math.min(Math.max(logoSize, 0.1), 0.3)\n  const shown = made && \"matrix\" in made ? made.matrix : null\n  const total = shown ? shown.size + quietZone * 2 : 0\n\n  const parts = React.useMemo(() => {\n    if (!shown) return null\n    const m = shown\n    const c = (m.size - 1) / 2\n    const half = (m.size * clamp) / 2\n    const inLogo = (x: number, y: number) => !!logo && Math.abs(x - c) <= half && Math.abs(y - c) <= half\n    const isFinder = (x: number, y: number) => (x < 8 && y < 8) || (x >= m.size - 8 && y < 8) || (x < 8 && y >= m.size - 8)\n    const skip = (x: number, y: number) => inLogo(x, y) || (shape !== \"square\" ? isFinder(x, y) : false)\n    const body = shape === \"square\" ? squarePath(m, (x, y) => inLogo(x, y)) : shape === \"rounded\" ? roundedPath(m, skip) : dotsPath(m, skip)\n    // the finders are drawn whole, the separator row around them stays empty\n    const finders = shape === \"square\" ? \"\" : `${finder(0, 0, shape)}${finder(m.size - 7, 0, shape)}${finder(0, m.size - 7, shape)}`\n    return { body, finders }\n  }, [shown, shape, logo, clamp])\n\n  const name = label ?? msg(\"qr-code.label\", \"QR code for {value}\", { value: value.length > 80 ? `${value.slice(0, 77)}…` : value })\n\n  if (!value) {\n    return (\n      <div data-slot=\"qr-code\" data-state=\"empty\" className={cn(\"flex items-center justify-center rounded-xl border border-dashed text-center text-sm text-muted-foreground\", className)} style={{ width: size, height: size, ...style }} {...props}>\n        {msg(\"qr-code.empty\", \"Nothing to encode yet\")}\n      </div>\n    )\n  }\n  if (failed || !shown || !parts) {\n    return (\n      <div data-slot=\"qr-code\" data-state=\"error\" role=\"alert\" className={cn(\"flex items-center justify-center rounded-xl border border-destructive/40 bg-destructive/5 p-3 text-center text-sm text-destructive\", className)} style={{ width: size, height: size, ...style }} {...props}>\n        {msg(\"qr-code.tooLong\", \"This is too long to fit in a QR code. Use a shorter link.\")}\n      </div>\n    )\n  }\n\n  const unit = size / total\n  const quiet = quietZone\n  return (\n    <div data-slot=\"qr-code\" data-shape={shape} className={cn(\"relative inline-block shrink-0 overflow-hidden rounded-xl\", className)} style={{ width: size, height: size, background, ...style }} {...props}>\n      <svg ref={svgRef} role=\"img\" aria-label={name} viewBox={`0 0 ${total} ${total}`} width={size} height={size} shapeRendering={shape === \"square\" ? \"crispEdges\" : \"geometricPrecision\"} xmlns=\"http://www.w3.org/2000/svg\">\n        <title>{name}</title>\n        <rect width={total} height={total} fill={background} />\n        <g transform={`translate(${quiet} ${quiet})`} fill={foreground}>\n          <path d={parts.body} />\n          {parts.finders ? <path d={parts.finders} fillRule=\"evenodd\" /> : null}\n        </g>\n      </svg>\n      {logo ? (\n        <div\n          data-slot=\"qr-code-logo\"\n          className=\"absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center overflow-hidden rounded-lg\"\n          style={{ background, width: Math.round(unit * shown.size * clamp), height: Math.round(unit * shown.size * clamp) }}\n        >\n          {logo}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\n/** The code as an SVG file's text, to save or send: `value`, with the same options as the component. */\nexport function qrToSvg(value: string, { level = \"M\", shape = \"square\", quietZone = 4, size = 512, foreground = \"rgb(0, 0, 0)\", background = \"rgb(255, 255, 255)\" }: Partial<Pick<QRCodeProps, \"level\" | \"shape\" | \"quietZone\" | \"size\" | \"foreground\" | \"background\">> = {}) {\n  const made = makeMatrix(value, level)\n  if (\"error\" in made) throw made.error\n  const m = made.matrix\n  const total = m.size + quietZone * 2\n  const none = () => false\n  const body = shape === \"square\" ? squarePath(m, none) : shape === \"rounded\" ? roundedPath(m, (x, y) => (x < 8 && y < 8) || (x >= m.size - 8 && y < 8) || (x < 8 && y >= m.size - 8)) : dotsPath(m, (x, y) => (x < 8 && y < 8) || (x >= m.size - 8 && y < 8) || (x < 8 && y >= m.size - 8))\n  const finders = shape === \"square\" ? \"\" : `${finder(0, 0, shape)}${finder(m.size - 7, 0, shape)}${finder(0, m.size - 7, shape)}`\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${total} ${total}\" width=\"${size}\" height=\"${size}\"><rect width=\"${total}\" height=\"${total}\" fill=\"${background}\"/><g transform=\"translate(${quietZone} ${quietZone})\" fill=\"${foreground}\"><path d=\"${body}\"/>${finders ? `<path d=\"${finders}\" fill-rule=\"evenodd\"/>` : \"\"}</g></svg>`\n}\n\n/**\n * Save the code the person is looking at, as an SVG or a PNG. Call it from a button's click. A PNG is drawn on a canvas at `size` pixels.\n */\nexport async function downloadQRCode(svg: SVGSVGElement, { filename = \"qr-code\", format = \"png\", size = 1024 }: { filename?: string; format?: \"svg\" | \"png\"; size?: number } = {}) {\n  const xml = new XMLSerializer().serializeToString(svg)\n  const save = (blob: Blob, ext: string) => {\n    const url = URL.createObjectURL(blob)\n    const a = document.createElement(\"a\")\n    a.href = url\n    a.download = `${filename}.${ext}`\n    a.click()\n    setTimeout(() => URL.revokeObjectURL(url), 1000)\n  }\n  if (format === \"svg\") return save(new Blob([xml], { type: \"image/svg+xml\" }), \"svg\")\n  const img = new Image()\n  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`\n  await img.decode()\n  const canvas = document.createElement(\"canvas\")\n  canvas.width = canvas.height = size\n  const ctx = canvas.getContext(\"2d\")\n  if (!ctx) throw new Error(\"A canvas is not available.\")\n  ctx.imageSmoothingEnabled = false\n  ctx.drawImage(img, 0, 0, size, size)\n  await new Promise<void>((resolve, reject) => canvas.toBlob((b) => (b ? (save(b, \"png\"), resolve()) : reject(new Error(\"Could not make the image.\"))), \"image/png\"))\n}\n\nexport { QRCode, type QRCodeLevel, type QRCodeProps, type QRCodeShape }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/qr-code.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "qr",
      "code",
      "svg",
      "scan",
      "share",
      "download",
      "wifi"
    ],
    "version": "1.0.0",
    "updated": "2026-10-19",
    "tests": true,
    "ai": {
      "summary": "<QRCode value=\"https://…\" size={192} />. shape is square, rounded or dots; level is L, M, Q or H; logo is a node drawn in the middle. Use svgRef with downloadQRCode(svg, { format }) for a save button.",
      "whenToUse": [
        "Sharing a link, a Wi-Fi network, a ticket or a pairing code to a phone",
        "Download or print screens that need a crisp code at any size",
        "A branded code with a logo in the middle"
      ],
      "whenNotToUse": [
        "Reading or scanning codes; this only draws them",
        "Very long text; shorten it first (a link shortener or an id)",
        "A barcode of another kind; this is QR only"
      ],
      "composesWith": [
        "button",
        "card",
        "dialog"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "The code is an image named \"QR code for …\"; label replaces the name when the value is long or private"
        },
        {
          "keys": "Always give a text alternative",
          "action": "Show the link or code as text near the picture, for people who cannot scan"
        }
      ],
      "customization": [
        "size, level, quietZone (keep at least 4)",
        "shape: square | rounded | dots",
        "foreground and background: keep dark on light so it scans in dark mode",
        "logo and logoSize (up to 0.3)"
      ]
    },
    "states": [
      {
        "id": "empty",
        "title": "Nothing yet",
        "example": "qr-code-empty",
        "trigger": "value=\"\"",
        "note": "A dashed box that says there is nothing to encode yet, the same size as the code so nothing jumps."
      },
      {
        "id": "error",
        "title": "Too long",
        "example": "qr-code-error",
        "trigger": "a value longer than a QR code holds",
        "note": "A red box that says the text is too long and to use a shorter link, announced as an alert; onError gets the error."
      }
    ],
    "source": {
      "name": "uqr",
      "url": "https://github.com/unjs/uqr",
      "license": "MIT",
      "copyright": "Copyright (c) 2023 Anthony Fu",
      "modified": true
    },
    "examples": [
      "qr-code-demo",
      "qr-code-shapes",
      "qr-code-logo",
      "qr-code-live",
      "qr-code-empty",
      "qr-code-error"
    ],
    "url": "https://ui.ballmac.com/components/qr-code"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}