velvet-qr 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pooja Gohel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,603 @@
1
+ # Velvet QR
2
+
3
+ > Generate beautiful QR codes effortlessly.
4
+
5
+ Part of the **Velvet** ecosystem of React developer libraries β€” `velvet-qr` is the easiest, most customizable, developer-friendly QR code component for React. Gradients, dot & eye styles, logos, and PNG/SVG/JPEG export, all fully typed.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/velvet-qr.svg)](https://www.npmjs.com/package/velvet-qr)
8
+ [![license](https://img.shields.io/npm/l/velvet-qr.svg)](./LICENSE)
9
+
10
+ **[Live demo & docs β†’](https://poojagohel.github.io/velvet-qr/)**
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - 🎨 **Fully customizable** β€” colors, gradients, dot styles, eye styles, rounded corners, transparent backgrounds
17
+ - 🧡 **Connected dot rendering** β€” "rounded"/"dots"/"classy"/"classy-rounded"/"extra-rounded" styles merge adjacent modules into a smooth connected shape, not isolated blobs
18
+ - πŸ–ΌοΈ **Logo embedding** β€” center a logo with configurable size, padding, and border radius, with automatic module excavation for scannability
19
+ - 🧩 **Presets** β€” `QR_PRESETS` + `QRPresetPicker` for ready-made styles (gradient "Velvet" look, dark-mode "Midnight", and more)
20
+ - πŸ—‚οΈ **Batch generation** β€” `useQRBatch` / `QRBatchGenerator` for generating and bulk-downloading many QR codes at once
21
+ - 🏷️ **Frame / label templates** β€” `frame` prop bakes a "SCAN ME"-style banner or bordered label directly into the exported SVG/PNG/JPEG, so it survives print/flat-image sharing
22
+ - πŸ“· **QR scanner** β€” `useQRScanner` / `QRScanner` decode QR codes from a live camera or a static image, using the browser-native `BarcodeDetector` API where available and transparently falling back to the bundled `jsQR` decoder everywhere else β€” works in Chrome, Edge, Firefox, and Safari alike
23
+ - πŸ•“ **Local QR history** β€” `useQRHistory` / `QRHistoryPanel` remember recently generated configs in `localStorage` for quick re-use
24
+ - πŸ“Š **Opt-in local analytics** β€” `useQRAnalytics` tracks generate/download/copy/in-session-scan events *you* observe in your own page β€” see the honesty note below, it cannot see real-world camera scans
25
+ - πŸ”— **Dynamic QR helpers** β€” `buildDynamicQrValue` / `useDynamicQr` help you build a QR that points at a stable URL under your own redirect endpoint, so the destination can change after printing (requires your own backend β€” see below)
26
+ - πŸ“¦ **Built-in content builders** β€” URL, plain text, email, phone, SMS, WiFi, vCard, and Google Maps/geo location
27
+ - ⬇️ **One-line exports** β€” download as PNG, SVG, or JPEG, or copy the image straight to the clipboard
28
+ - πŸͺ **Hooks-first API** β€” `useQRGenerator`, `useQRDownload`, and `useQRBatch` for full control when you don't want the batteries-included components
29
+ - πŸ”’ **Strongly typed** β€” complete TypeScript definitions, no `any`
30
+ - ⚑ **Tiny & tree-shakeable** β€” two runtime dependencies (`qrcode` for matrix encoding, `jsqr` for the scanner's cross-browser fallback decoding), ESM + CJS builds, `"use client"` banner for Next.js App Router
31
+ - β™Ώ **Accessible** β€” proper `role="img"` / `aria-label` on generated SVGs, including an accessible placeholder if `value` is empty or generation fails
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install velvet-qr
37
+ # or
38
+ yarn add velvet-qr
39
+ # or
40
+ pnpm add velvet-qr
41
+ ```
42
+
43
+ Requires React 18 or 19.
44
+
45
+ ## Quick Start
46
+
47
+ ```tsx
48
+ import { VelvetQR } from "velvet-qr";
49
+
50
+ export default function Example() {
51
+ return (
52
+ <VelvetQR
53
+ value="https://poojagohel.github.io/velvet-qr/"
54
+ size={250}
55
+ foregroundColor="#000"
56
+ backgroundColor="#fff"
57
+ />
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### Downloading & copying
63
+
64
+ `VelvetQR` exposes an imperative handle via `ref` β€” no extra wiring required:
65
+
66
+ ```tsx
67
+ import { useRef } from "react";
68
+ import { VelvetQR, type VelvetQRHandle } from "velvet-qr";
69
+
70
+ function DownloadableQR() {
71
+ const qrRef = useRef<VelvetQRHandle>(null);
72
+
73
+ return (
74
+ <>
75
+ <VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" size={250} />
76
+ <button onClick={() => qrRef.current?.downloadPNG()}>Download PNG</button>
77
+ <button onClick={() => qrRef.current?.downloadSVG()}>Download SVG</button>
78
+ <button onClick={() => qrRef.current?.downloadJPEG()}>Download JPEG</button>
79
+ <button onClick={() => qrRef.current?.copyToClipboard()}>Copy image</button>
80
+ </>
81
+ );
82
+ }
83
+ ```
84
+
85
+ Or use the ready-made toolbar:
86
+
87
+ ```tsx
88
+ import { useRef } from "react";
89
+ import { VelvetQR, QRDownloader, type VelvetQRHandle } from "velvet-qr";
90
+
91
+ function Example() {
92
+ const qrRef = useRef<VelvetQRHandle>(null);
93
+ return (
94
+ <>
95
+ <VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" />
96
+ <QRDownloader qrRef={qrRef} />
97
+ </>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Gradients, dot styles, and a logo
103
+
104
+ ```tsx
105
+ <VelvetQR
106
+ value="https://poojagohel.github.io/velvet-qr/"
107
+ size={280}
108
+ dotStyle="extra-rounded"
109
+ eyeStyle="leaf"
110
+ cornerRadius={24}
111
+ gradient={{ type: "linear", colors: ["#7c3aed", "#ec4899"], rotation: 45 }}
112
+ logoImage="/logo.png"
113
+ logoSize={0.22}
114
+ logoPadding={6}
115
+ logoBorderRadius={12}
116
+ />
117
+ ```
118
+
119
+ ### Content builders
120
+
121
+ Every common QR use case has a small, pure helper that returns the correctly formatted string for `value`:
122
+
123
+ ```tsx
124
+ import {
125
+ VelvetQR,
126
+ buildWifiValue,
127
+ buildVCardValue,
128
+ buildEmailValue,
129
+ buildSmsValue,
130
+ buildPhoneValue,
131
+ buildLocationValue,
132
+ buildGoogleMapsValue,
133
+ } from "velvet-qr";
134
+
135
+ // WiFi
136
+ <VelvetQR value={buildWifiValue({ ssid: "Velvet Cafe", password: "s3cret!", encryption: "WPA" })} />
137
+
138
+ // vCard / business card
139
+ <VelvetQR
140
+ value={buildVCardValue({
141
+ firstName: "Pooja",
142
+ lastName: "Gohel",
143
+ org: "Velvet UI",
144
+ title: "Founder",
145
+ phone: "+1 555 0100",
146
+ email: "hello@velvetui.dev",
147
+ website: "https://poojagohel.github.io/velvet-qr/",
148
+ })}
149
+ />
150
+
151
+ // Email, SMS, phone, location
152
+ <VelvetQR value={buildEmailValue({ to: "hello@velvetui.dev", subject: "Hi!" })} />
153
+ <VelvetQR value={buildSmsValue({ phone: "+15550100", message: "Hello!" })} />
154
+ <VelvetQR value={buildPhoneValue("+15550100")} />
155
+ <VelvetQR value={buildLocationValue({ latitude: 37.7749, longitude: -122.4194 })} />
156
+ <VelvetQR value={buildGoogleMapsValue({ query: "Golden Gate Bridge" })} />
157
+ ```
158
+
159
+ ### The customization panel
160
+
161
+ ```tsx
162
+ import { useState } from "react";
163
+ import { VelvetQR, QRCustomizer, type QRCustomizerConfig } from "velvet-qr";
164
+
165
+ function Playground() {
166
+ const [config, setConfig] = useState<QRCustomizerConfig>({ value: "https://poojagohel.github.io/velvet-qr/" });
167
+ return (
168
+ <>
169
+ <VelvetQR {...config} />
170
+ <QRCustomizer value={config} onChange={setConfig} />
171
+ </>
172
+ );
173
+ }
174
+ ```
175
+
176
+ ### Logo uploader
177
+
178
+ ```tsx
179
+ import { useState } from "react";
180
+ import { VelvetQR, QRLogoUploader } from "velvet-qr";
181
+
182
+ function WithLogo() {
183
+ const [logo, setLogo] = useState<string | null>(null);
184
+ return (
185
+ <>
186
+ <VelvetQR value="https://poojagohel.github.io/velvet-qr/" logoImage={logo ?? undefined} />
187
+ <QRLogoUploader onLogoChange={setLogo} />
188
+ </>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ### Presets
194
+
195
+ Skip the styling decisions entirely with a curated set of ready-made looks:
196
+
197
+ ```tsx
198
+ import { useState } from "react";
199
+ import { VelvetQR, QRPresetPicker, type VelvetQRProps } from "velvet-qr";
200
+
201
+ function PresetDemo() {
202
+ const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
203
+ return (
204
+ <>
205
+ <VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...config} />
206
+ <QRPresetPicker onSelect={setConfig} />
207
+ </>
208
+ );
209
+ }
210
+ ```
211
+
212
+ Or apply one directly without the picker UI:
213
+
214
+ ```tsx
215
+ import { VelvetQR, QR_PRESETS } from "velvet-qr";
216
+
217
+ const velvet = QR_PRESETS.find((p) => p.id === "velvet")!;
218
+ <VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...velvet.config} />;
219
+ ```
220
+
221
+ ### Batch generation
222
+
223
+ Generate and bulk-download many QR codes at once:
224
+
225
+ ```tsx
226
+ import { QRBatchGenerator } from "velvet-qr";
227
+
228
+ function Batch() {
229
+ return (
230
+ <QRBatchGenerator
231
+ items={[
232
+ { id: "table-1", value: "https://poojagohel.github.io/velvet-qr/menu?table=1" },
233
+ { id: "table-2", value: "https://poojagohel.github.io/velvet-qr/menu?table=2" },
234
+ { id: "table-3", value: "https://poojagohel.github.io/velvet-qr/menu?table=3" },
235
+ ]}
236
+ sharedOptions={{ dotStyle: "rounded", eyeStyle: "circle" }}
237
+ />
238
+ );
239
+ }
240
+ ```
241
+
242
+ Or drive it yourself with the underlying hook:
243
+
244
+ ```tsx
245
+ import { useQRBatch } from "velvet-qr";
246
+
247
+ function CustomBatch() {
248
+ const { results, downloadAll } = useQRBatch(
249
+ [{ id: "a", value: "https://a.example" }, { id: "b", value: "https://b.example" }],
250
+ );
251
+ // render results.map(r => r.svgMarkup) however you like
252
+ // downloadAll('png' | 'svg' | 'jpeg') triggers one sequential download per item
253
+ }
254
+ ```
255
+
256
+ ### Hooks β€” full control
257
+
258
+ ```tsx
259
+ import { useQRGenerator, useQRDownload } from "velvet-qr";
260
+
261
+ function Custom() {
262
+ const { svgMarkup, getDataUrl, isGenerating, error } = useQRGenerator({
263
+ value: "https://poojagohel.github.io/velvet-qr/",
264
+ size: 240,
265
+ });
266
+ // render svgMarkup however you like, or call getDataUrl('png' | 'jpeg' | 'svg')
267
+ }
268
+ ```
269
+
270
+ ### Frame / label templates
271
+
272
+ Bake a "SCAN ME" caption directly into the exported image β€” it's part of the generated SVG
273
+ (not a separate on-screen wrapper), so it's included in PNG/SVG/JPEG downloads too:
274
+
275
+ ```tsx
276
+ <VelvetQR
277
+ value="https://poojagohel.github.io/velvet-qr/"
278
+ size={240}
279
+ frame={{ style: "banner-bottom", text: "SCAN ME" }}
280
+ />
281
+ ```
282
+
283
+ `frame.style` is one of `"none"` (default), `"banner-top"`, `"banner-bottom"`, or
284
+ `"border-label"`. `"banner-top"`/`"banner-bottom"` grow the rendered SVG to
285
+ `size` wide Γ— `size + size * 0.2` tall (the caption band is ~20% of `size`); `"border-label"`
286
+ keeps the SVG at `size` Γ— `size` and instead draws a border with the label in a small pill
287
+ straddling its bottom edge. Because banner styles change the overall height, always read the
288
+ actual `viewBox`/`width`/`height` off the generated markup (or the size `VelvetQR`'s own
289
+ `downloadPNG`/`downloadJPEG`/`getDataUrl` methods rasterize to) rather than assuming a square
290
+ `size x size` canvas when doing layout. Keep `frame.text` to roughly 20 characters or fewer
291
+ (14 or fewer for `border-label`) for reliably legible results β€” longer text is automatically
292
+ shrunk and, if needed, truncated with an ellipsis.
293
+
294
+ ```tsx
295
+ <VelvetQR
296
+ value="https://poojagohel.github.io/velvet-qr/"
297
+ size={240}
298
+ frame={{
299
+ style: "border-label",
300
+ text: "SCAN ME",
301
+ backgroundColor: "#7c3aed",
302
+ textColor: "#ffffff",
303
+ cornerRadius: 16,
304
+ }}
305
+ />
306
+ ```
307
+
308
+ ### Animation
309
+
310
+ Add a purely decorative, on-screen-only reveal or "alive" effect for a landing-page hero β€”
311
+ it never affects scannability, and **exports (PNG/SVG/JPEG downloads, `getDataUrl`, and batch
312
+ `downloadAll`) are always the fully-settled, static QR regardless of `animation`**:
313
+
314
+ ```tsx
315
+ <VelvetQR
316
+ value="https://poojagohel.github.io/velvet-qr/"
317
+ size={240}
318
+ animation={{ type: "fade-in" }} // "fade-in" | "draw-in" | "pulse" | "gradient-shift"
319
+ />
320
+ ```
321
+
322
+ - `"fade-in"` / `"draw-in"`: a one-shot staggered reveal (data modules fade in, or fade+scale
323
+ in) that plays once and settles at fully visible forever after.
324
+ - `"pulse"`: a continuous, gentle looping "breathing" effect for hero sections β€” never applied
325
+ to a `logoImage` if one is set, so the logo stays static/legible.
326
+ - `"gradient-shift"`: continuously rotates a `gradient` (no-ops back to `"none"` if `gradient`
327
+ isn't also set).
328
+
329
+ All animation types respect `prefers-reduced-motion: reduce`, falling back to the static,
330
+ fully-settled render. Tune timing with `duration` (seconds) and, for the reveal types,
331
+ `staggerDelay` (seconds of extra delay per module):
332
+
333
+ ```tsx
334
+ <VelvetQR value="https://poojagohel.github.io/velvet-qr/" animation={{ type: "draw-in", duration: 1.5, staggerDelay: 0.004 }} />
335
+ ```
336
+
337
+ ### Scanner
338
+
339
+ Decode QR codes from a live camera or a static image. Uses the browser-native
340
+ `BarcodeDetector` API where available (Chrome/Edge) and transparently falls back to the
341
+ bundled `jsQR` decoder everywhere else (Firefox, Safari, and any other browser), so scanning
342
+ works across all major browsers:
343
+
344
+ ```tsx
345
+ import { QRScanner } from "velvet-qr";
346
+
347
+ function Scan() {
348
+ return (
349
+ <QRScanner
350
+ onDetected={(value) => console.log("Scanned:", value)}
351
+ continuous={false}
352
+ />
353
+ );
354
+ }
355
+ ```
356
+
357
+ Or drive it yourself with the underlying hook:
358
+
359
+ ```tsx
360
+ import { useRef } from "react";
361
+ import { useQRScanner } from "velvet-qr";
362
+
363
+ function CustomScanner() {
364
+ const { isSupported, isScanning, result, error, startCamera, stopCamera, scanImage } = useQRScanner();
365
+ const videoRef = useRef<HTMLVideoElement>(null);
366
+
367
+ if (!isSupported) return <p>Camera scanning isn't supported in this browser.</p>;
368
+
369
+ return (
370
+ <>
371
+ <video ref={videoRef} muted playsInline />
372
+ <button onClick={() => videoRef.current && startCamera(videoRef.current)}>Start</button>
373
+ <button onClick={stopCamera}>Stop</button>
374
+ <input type="file" accept="image/*" onChange={(e) => e.target.files?.[0] && scanImage(e.target.files[0])} />
375
+ {result && <p>Scanned: {result}</p>}
376
+ {error && <p>Error: {error}</p>}
377
+ </>
378
+ );
379
+ }
380
+ ```
381
+
382
+ `isSupported` is `false` (rather than throwing) only when neither `BarcodeDetector` nor the
383
+ `jsQR` canvas fallback can run at all β€” in practice that's just non-browser/SSR
384
+ environments. Every real browser reports `true`. Still always render a fallback (e.g. the
385
+ `QRScanner` component's built-in file-input path) for the rare environments where it isn't.
386
+
387
+ ### History
388
+
389
+ `useQRHistory` keeps a small local record of recently generated/used QR configurations in
390
+ `localStorage` (SSR-safe, capped at `maxEntries`), and `QRHistoryPanel` renders it as a row of
391
+ live, clickable thumbnails:
392
+
393
+ ```tsx
394
+ import { useState } from "react";
395
+ import { VelvetQR, useQRHistory, QRHistoryPanel, type VelvetQRProps } from "velvet-qr";
396
+
397
+ function HistoryDemo() {
398
+ const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
399
+ const [value, setValue] = useState("https://poojagohel.github.io/velvet-qr/");
400
+ const history = useQRHistory({ maxEntries: 20 });
401
+
402
+ return (
403
+ <>
404
+ <VelvetQR value={value} {...config} />
405
+ <button onClick={() => history.add({ value, config })}>Save to history</button>
406
+ <QRHistoryPanel
407
+ history={history}
408
+ onSelect={(entry) => {
409
+ setValue(entry.value);
410
+ setConfig(entry.config);
411
+ }}
412
+ />
413
+ </>
414
+ );
415
+ }
416
+ ```
417
+
418
+ This is purely local bookkeeping of configs *you* explicitly `add()` β€” it has no relationship
419
+ to whether the resulting QR code has ever been scanned by anyone.
420
+
421
+ ### Analytics β€” honestly scoped
422
+
423
+ > **This package is a client-side React component library with no backend and no hosting.**
424
+ > It is architecturally impossible for it to detect when someone scans a printed or shared QR
425
+ > code with their own phone camera β€” that scan happens entirely outside any page this
426
+ > library's JS ever runs in. `useQRAnalytics` only tracks events **observable within your own
427
+ > page session**: generation, exports (PNG/SVG/JPEG), clipboard copies, and in-session decodes
428
+ > if you also use this package's own `useQRScanner`/`QRScanner`. Real "was my printed QR code
429
+ > scanned" tracking requires server-side infrastructure outside this package's scope β€” e.g.
430
+ > routing the QR's destination through your own backend redirect endpoint and logging hits
431
+ > there (see [Dynamic QR](#dynamic-qr--honestly-scoped) below).
432
+
433
+ `useQRAnalytics` does not wrap or auto-instrument `VelvetQR` β€” you call `track()` explicitly,
434
+ wherever you already have the relevant information:
435
+
436
+ ```tsx
437
+ import { useRef } from "react";
438
+ import { VelvetQR, useQRAnalytics, type VelvetQRHandle } from "velvet-qr";
439
+
440
+ function AnalyticsDemo() {
441
+ const qrRef = useRef<VelvetQRHandle>(null);
442
+ const { track, stats } = useQRAnalytics({
443
+ persist: true, // aggregate local counts into localStorage
444
+ onTrack: (event) => myAnalyticsBackend.send(event), // pipe to GA/PostHog/your own backend
445
+ });
446
+
447
+ return (
448
+ <>
449
+ <VelvetQR
450
+ ref={qrRef}
451
+ value="https://poojagohel.github.io/velvet-qr/"
452
+ onGenerated={() => track({ type: "generate", value: "https://poojagohel.github.io/velvet-qr/" })}
453
+ />
454
+ <button
455
+ onClick={async () => {
456
+ await qrRef.current?.downloadPNG();
457
+ track({ type: "download", value: "https://poojagohel.github.io/velvet-qr/", format: "png" });
458
+ }}
459
+ >
460
+ Download PNG
461
+ </button>
462
+ <p>Generated locally {stats.byType.generate} times this session/device.</p>
463
+ </>
464
+ );
465
+ }
466
+ ```
467
+
468
+ ### Dynamic QR β€” honestly scoped
469
+
470
+ A "dynamic QR" (one whose destination can change after printing) needs the printed code to
471
+ always point at a **stable URL your own backend controls**, so the backend β€” not this
472
+ package β€” decides where to redirect at scan time. `buildDynamicQrValue` builds that stable
473
+ URL:
474
+
475
+ ```tsx
476
+ import { VelvetQR, buildDynamicQrValue } from "velvet-qr";
477
+
478
+ <VelvetQR value={buildDynamicQrValue({ baseUrl: "https://yourapp.com/r", id: "table-12" })} />;
479
+ // -> encodes "https://yourapp.com/r/table-12" β€” your server's /r/table-12 route decides
480
+ // where to redirect, and can change that destination at any time without reprinting the code.
481
+ ```
482
+
483
+ `useDynamicQr` is a small convenience wrapper around that, with a **localStorage-backed
484
+ resolver meant only for local prototyping/demos** of the "swap the destination" experience β€”
485
+ it does *not* make the physical/printed code dynamic for anyone else scanning it with a real
486
+ camera, since that requires a real server-side redirect endpoint:
487
+
488
+ ```tsx
489
+ import { VelvetQR, useDynamicQr } from "velvet-qr";
490
+
491
+ function DynamicQrDemo() {
492
+ const { value, destination, isLoading, setDestination } = useDynamicQr({
493
+ id: "table-12",
494
+ baseUrl: "https://yourapp.com/r",
495
+ // resolver: (id) => fetch(`/api/dynamic-links/${id}`).then((r) => r.json()).then((d) => d.destination),
496
+ });
497
+
498
+ return (
499
+ <>
500
+ <VelvetQR value={value} />
501
+ <p>{isLoading ? "Loading…" : `Currently redirects to: ${destination ?? "(unset)"}`}</p>
502
+ <button onClick={() => setDestination("https://example.com/new-menu")}>
503
+ Update destination (localStorage demo only)
504
+ </button>
505
+ </>
506
+ );
507
+ }
508
+ ```
509
+
510
+ Pass your own `resolver` (backed by a real API) for production use β€” `setDestination` is a
511
+ no-op (with a `console.warn`) when a custom `resolver` is supplied, since updating the real
512
+ destination is your backend's job, not this hook's.
513
+
514
+ ## Props
515
+
516
+ ### `VelvetQR`
517
+
518
+ | Prop | Type | Default | Description |
519
+ | --- | --- | --- | --- |
520
+ | `value` | `string` | β€” | **Required.** The content to encode. Use the builder helpers for structured data (WiFi, vCard, etc). |
521
+ | `size` | `number` | `240` | Rendered width/height in pixels. Scales down responsively (`max-width: 100%`). Clamped to `> 0`. |
522
+ | `margin` | `number` | `4` | Quiet-zone size, in QR modules. Clamped to `>= 0`. |
523
+ | `backgroundColor` | `string` | `"#ffffff"` | Background fill color. Ignored if `transparentBackground` is set. |
524
+ | `foregroundColor` | `string` | `"#000000"` | Data module color. Ignored if `gradient` is set. |
525
+ | `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Higher levels tolerate more damage/logo overlap at the cost of density. Use `"H"` when embedding a logo. |
526
+ | `cornerRadius` | `number` | `0` | Corner radius (px) of the outer background rect. |
527
+ | `dotStyle` | `"square" \| "rounded" \| "dots" \| "classy" \| "classy-rounded" \| "extra-rounded"` | `"square"` | Shape of the data modules. |
528
+ | `eyeStyle` | `"square" \| "circle" \| "rounded" \| "leaf"` | `"square"` | Shape of the three finder-pattern "eyes". |
529
+ | `eyeColor` | `string` | `foregroundColor` | Override color for the eyes only. |
530
+ | `gradient` | `{ type: "linear" \| "radial"; colors: [string, string]; rotation?: number }` | β€” | Applies a gradient fill to data modules instead of `foregroundColor`. |
531
+ | `transparentBackground` | `boolean` | `false` | Omit the background rect entirely (transparent PNG/SVG exports). |
532
+ | `logoImage` | `string` | β€” | URL or data URL of a logo to embed at the center. |
533
+ | `logoSize` | `number` | `0.2` | Logo size as a fraction (0–1) of `size`. Clamped to `[0, 1]`. |
534
+ | `logoPadding` | `number` | `4` | Solid-color padding (px) around the logo. |
535
+ | `logoBorderRadius` | `number` | `8` | Corner radius (px) of the logo's padded backdrop. |
536
+ | `logoExcavate` | `boolean` | `true` | Skip rendering data modules directly under the logo so it stays legible. |
537
+ | `frame` | `{ style: "none" \| "banner-top" \| "banner-bottom" \| "border-label"; text?: string; backgroundColor?: string; textColor?: string; cornerRadius?: number }` | β€” (`"none"`) | Bakes a caption banner/border-label into the exported SVG/PNG/JPEG. See "Frame / label templates" above for the exact dimension contract. |
538
+ | `animation` | `{ type: "none" \| "fade-in" \| "draw-in" \| "pulse" \| "gradient-shift"; duration?: number; staggerDelay?: number }` | β€” (`"none"`) | Purely decorative, **on-screen-only** animation. See "Animation" above. **Exports (`downloadPNG`/`downloadSVG`/`downloadJPEG`/`getDataUrl`, and `useQRBatch`'s `downloadAll`) are always the fully-settled, static QR, regardless of this prop.** |
539
+ | `className` / `style` | β€” | β€” | Passed through to the root `<svg>`. |
540
+ | `ariaLabel` | `string` | `` `QR code for ${value}` `` | Accessible label on the root `<svg>` (`role="img"`). |
541
+ | `onGenerated` | `(svgMarkup: string) => void` | β€” | Called whenever the generated SVG markup changes. |
542
+ | `id` | `string` | auto-generated | Root element id; also used internally for gradient/clip-path def scoping. |
543
+
544
+ ### `VelvetQRHandle` (ref)
545
+
546
+ | Method | Description |
547
+ | --- | --- |
548
+ | `downloadPNG(filename?)` | Rasterizes the current SVG and downloads it as a PNG. |
549
+ | `downloadSVG(filename?)` | Downloads the raw SVG markup. |
550
+ | `downloadJPEG(filename?, quality?)` | Rasterizes and downloads as JPEG (`quality` 0–1, default `0.92`). |
551
+ | `copyToClipboard()` | Copies the rendered PNG to the system clipboard via the Clipboard API. |
552
+ | `getDataUrl(format, quality?)` | Returns a data URL (`"png" \| "jpeg" \| "svg"`) without triggering a download. |
553
+
554
+ ## Components
555
+
556
+ | Component | Purpose |
557
+ | --- | --- |
558
+ | `VelvetQR` | The core QR renderer. |
559
+ | `QRPreview` | Styled preview card wrapping `VelvetQR`. |
560
+ | `QRDownloader` | Ready-made download/copy toolbar bound to a `VelvetQR` ref. |
561
+ | `QRCustomizer` | Controlled customization panel (`value` + `onChange`) for building playgrounds/editors. |
562
+ | `QRLogoUploader` | Drag-and-drop / file-picker input that resolves an image file to a data URL. |
563
+ | `QRPresetPicker` | Row of live, clickable preset thumbnails calling `onSelect(config)`. |
564
+ | `QRBatchGenerator` | Grid of generated QR codes for a list of items, plus a "Download All" action. |
565
+ | `QRScanner` | Camera + static-image QR scanner UI built on `useQRScanner`, calling `onDetected(value)`. |
566
+ | `QRHistoryPanel` | Row of live thumbnails for `useQRHistory` entries, calling `onSelect(entry)`, with per-entry remove. |
567
+
568
+ ## Hooks
569
+
570
+ | Hook | Purpose |
571
+ | --- | --- |
572
+ | `useQRGenerator(options)` | Core generation hook: matrix + SVG markup + `getDataUrl`, without any component/DOM ref. |
573
+ | `useQRDownload(qrRef)` | Wraps a `VelvetQR` ref with `downloadPNG`/`downloadSVG`/`downloadJPEG`/`copyToClipboard`. |
574
+ | `useQRBatch(items, sharedOptions?)` | Generates `{ id, value, svgMarkup, error }` for each item, plus `downloadAll(format)`. |
575
+ | `useQRScanner()` | Decodes QR codes from a camera stream (`startCamera`/`stopCamera`) or a static image (`scanImage`) via the native `BarcodeDetector` API. `isSupported` is `false` when unavailable. |
576
+ | `useQRHistory(options?)` | Local, `localStorage`-backed history of QR configs: `entries`, `add`, `remove`, `clear`, `restore`. |
577
+ | `useQRAnalytics(options?)` | Opt-in, explicitly-called event tracking (`track`) for generate/download/copy/scan-decode events observable in your own session β€” **not** real-world camera scans. See "Analytics β€” honestly scoped" above. |
578
+ | `useDynamicQr(options)` | Builds a stable dynamic-QR `value` plus a `destination`/`setDestination`/`refresh` API; default resolver is `localStorage`-backed for prototyping only. See "Dynamic QR β€” honestly scoped" above. |
579
+
580
+ ## Presets
581
+
582
+ | Export | Purpose |
583
+ | --- | --- |
584
+ | `QR_PRESETS` | 8 curated `{ id, name, description, config }` presets β€” spread `.config` onto `<VelvetQR>`. |
585
+ | `QrPreset` | The preset type, for building your own preset list in the same shape. |
586
+
587
+ ## FAQ
588
+
589
+ **SVG, PNG, or JPEG β€” which should I export?** SVG for print/scalable use (logos, packaging); PNG when you need a transparent background or a raster file for the web; JPEG only if file size matters more than a transparent background (JPEG has no alpha channel).
590
+
591
+ **Will a logo make my QR code unscannable?** Keep `logoSize` under ~0.25 and use `errorCorrectionLevel="H"` (or at least `"Q"`) when embedding a logo β€” the extra error-correction data is what keeps the code scannable despite the excavated center.
592
+
593
+ **Does this work with Next.js / SSR?** Yes β€” the package ships a `"use client"` banner, so import `VelvetQR` directly in a Client Component. See `examples/nextjs-app-router` in the repo for a full walkthrough of why your own page still needs its own `"use client"` directive too.
594
+
595
+ **How big is the bundle?** `velvet-qr` has two runtime dependencies: `qrcode` (used only to compute the encoding matrix β€” all rendering is a hand-written SVG engine) and `jsqr` (~15KB, used only as the scanner's cross-browser fallback decoder when the native `BarcodeDetector` API isn't available). The whole package tree-shakes per export.
596
+
597
+ **Can velvet-qr track when my QR code gets scanned?** No β€” not a real-world scan of a printed or shared code. `velvet-qr` is a client-side component library with no backend and no hosting, so it never runs any code on the device that scans your printed QR β€” there's nothing for it to observe. `useQRAnalytics` can track events that happen *inside your own page*, in your own visitor's session (QR generation, PNG/SVG/JPEG exports, clipboard copies, and in-session decodes if you also use `useQRScanner`), but that's a different thing from "someone scanned my flyer." Real scan tracking requires your own server: point the QR at a stable URL under your own redirect endpoint (`buildDynamicQrValue`/`useDynamicQr`) and log hits there.
598
+
599
+ For deeper coverage (batch generation, presets, logo scannability tuning, error-correction-level tradeoffs), see [`docs/FAQ.md`](../docs/FAQ.md) in the repo root, alongside [`docs/INSTALLATION.md`](../docs/INSTALLATION.md), [`docs/API-REFERENCE.md`](../docs/API-REFERENCE.md), and [`docs/MIGRATION.md`](../docs/MIGRATION.md).
600
+
601
+ ## License
602
+
603
+ MIT Β© Pooja Gohel