Introducing Nuvyx UI v1.0.0

Image Scanner

A dynamic component that applies interactive scanning effects to images, supporting various scan patterns, directions, and customizable.

ImageScanner

Scanning image

Click to Scan

Click on the image to trigger the scan effect

Scanning image

Repeating Scan

Continuously scans in a loop

Scanning image

Button Triggered

Click the button to trigger scanning

Installation Guide

1

Install Dependencies

Framer Motion

npm install framer-motion

Utility Functions

npm install clsx tailwind-merge
2

Setup Configuration

Create file: /lib/utils.ts
/lib/utils.ts
Configuration
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5  return twMerge(clsx(inputs));
6}
3

Copy Component Code

Image Scanner.tsx
TypeScript
1"use client";
2
3import { useState, useEffect, useRef } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import { cn } from "@/lib/utils";
6import Image from "next/image";
7
8export interface ImageScannerProps {
9  image: string;
10  alt?: string;
11  scanDirection?: "horizontal" | "vertical";
12  scanSpeed?: number;
13  scanColor?: "emerald" | "blue" | "purple" | "amber" | "red";
14  scanType?: "line" | "corners" | "both";
15  className?: string;
16  onScanComplete?: () => void;
17  autoScan?: boolean;
18  scanDelay?: number;
19  scanAtScroll?: boolean;
20  repeating?: boolean;
21  triggerScan?: boolean;
22}
23
24export const ImageScanner = ({
25  image,
26  alt = "Scanning image",
27  scanDirection = "horizontal",
28  scanSpeed = 2,
29  scanColor = "emerald",
30  scanType = "both",
31  className,
32  onScanComplete,
33  autoScan = false,
34  scanDelay = 0,
35  scanAtScroll = false,
36  repeating = false,
37  triggerScan = false,
38}: ImageScannerProps) => {
39  const [isScanning, setIsScanning] = useState(false);
40  const [scanComplete, setScanComplete] = useState(false);
41  const [hasScanned, setHasScanned] = useState(false);
42  const [scanCycle, setScanCycle] = useState(0);
43  const ref = useRef<HTMLDivElement>(null);
44  const scanTimer = useRef<NodeJS.Timeout | null>(null);
45  const completeTimer = useRef<NodeJS.Timeout | null>(null);
46
47  const colorMap = {
48    emerald: {
49      scan: "bg-emerald-500",
50      glow: "bg-emerald-500/20 dark:bg-emerald-500/10",
51      border: "border-emerald-500",
52    },
53    blue: {
54      scan: "bg-blue-500",
55      glow: "bg-blue-500/20 dark:bg-blue-500/10",
56      border: "border-blue-500",
57    },
58    purple: {
59      scan: "bg-purple-500",
60      glow: "bg-purple-500/20 dark:bg-purple-500/10",
61      border: "border-purple-500",
62    },
63    amber: {
64      scan: "bg-amber-500",
65      glow: "bg-amber-500/20 dark:bg-amber-500/10",
66      border: "border-amber-500",
67    },
68    red: {
69      scan: "bg-red-500",
70      glow: "bg-red-500/20 dark:bg-red-500/10",
71      border: "border-red-500",
72    },
73  };
74
75  const runScan = () => {
76    if (!isScanning) {
77      setIsScanning(true);
78      setScanCycle((prev) => prev + 1);
79
80      completeTimer.current = setTimeout(() => {
81        setScanComplete(true);
82        setHasScanned(true);
83        if (onScanComplete) onScanComplete();
84
85        setTimeout(() => {
86          setScanComplete(false);
87          setIsScanning(false);
88          if (repeating) {
89            scanTimer.current = setTimeout(runScan, 1000);
90          }
91        }, 1000);
92      }, scanSpeed * 1000);
93    }
94  };
95
96  useEffect(() => {
97    if (!scanAtScroll || !ref.current) return;
98
99    const observer = new IntersectionObserver(
100      (entries) => {
101        const [entry] = entries;
102        if (entry.isIntersecting && !hasScanned && !isScanning) {
103          runScan();
104        }
105      },
106      { threshold: 0.5 }
107    );
108
109    observer.observe(ref.current);
110    return () => observer.disconnect();
111    // eslint-disable-next-line react-hooks/exhaustive-deps
112  }, [scanAtScroll, hasScanned, isScanning]);
113
114  useEffect(() => {
115    if (autoScan && !hasScanned) {
116      scanTimer.current = setTimeout(runScan, scanDelay * 1000);
117    }
118
119    return () => {
120      if (scanTimer.current) clearTimeout(scanTimer.current);
121      if (completeTimer.current) clearTimeout(completeTimer.current);
122    };
123    // eslint-disable-next-line react-hooks/exhaustive-deps
124  }, [autoScan, scanDelay, hasScanned]);
125
126  useEffect(() => {
127    if (triggerScan && !isScanning) {
128      runScan();
129    }
130    // eslint-disable-next-line react-hooks/exhaustive-deps
131  }, [triggerScan]);
132
133  useEffect(() => {
134    if (repeating && !isScanning && !scanComplete) {
135      runScan();
136    }
137
138    return () => {
139      if (scanTimer.current) clearTimeout(scanTimer.current);
140      if (completeTimer.current) clearTimeout(completeTimer.current);
141    };
142    // eslint-disable-next-line react-hooks/exhaustive-deps
143  }, [repeating]);
144
145  useEffect(() => {
146    return () => {
147      if (scanTimer.current) clearTimeout(scanTimer.current);
148      if (completeTimer.current) clearTimeout(completeTimer.current);
149    };
150  }, []);
151
152  const startScan = () => {
153    if (!isScanning && !autoScan && !repeating) {
154      runScan();
155    }
156  };
157  const selectedColor = colorMap[scanColor] || colorMap.emerald;
158
159  return (
160    <div
161      ref={ref}
162      className={cn("relative overflow-hidden", className)}
163      onClick={!autoScan && !scanAtScroll && !repeating ? startScan : undefined}
164    >
165      <div
166        className={cn(
167          "w-full h-full relative overflow-hidden",
168          scanComplete ? "ring-2 ring-offset-2 dark:ring-offset-gray-900" : "",
169          scanComplete ? selectedColor.border : ""
170        )}
171      >
172        <Image
173          src={image || "/placeholder.svg"}
174          alt={alt}
175          height={500}
176          width={500}
177          quality={100}
178          className="w-full h-full object-cover"
179        />
180        <AnimatePresence mode="wait">
181          {isScanning && (
182            <>
183              {(scanType === "line" || scanType === "both") && (
184                <motion.div
185                  key={`scanline-${scanCycle}`}
186                  className={cn(
187                    "absolute pointer-events-none",
188                    scanDirection === "horizontal"
189                      ? "left-0 right-0 h-1"
190                      : "top-0 bottom-0 w-1",
191                    selectedColor.scan
192                  )}
193                  initial={
194                    scanDirection === "horizontal"
195                      ? { top: 0, opacity: 0.7 }
196                      : { left: 0, opacity: 0.7 }
197                  }
198                  animate={
199                    scanDirection === "horizontal"
200                      ? { top: "100%", opacity: 0.7 }
201                      : { left: "100%", opacity: 0.7 }
202                  }
203                  exit={
204                    scanDirection === "horizontal"
205                      ? { top: "100%", opacity: 0 }
206                      : { left: "100%", opacity: 0 }
207                  }
208                  transition={{
209                    duration: scanSpeed,
210                    ease: "linear",
211                  }}
212                />
213              )}
214              {(scanType === "corners" || scanType === "both") && (
215                <>
216                  <motion.div
217                    key={`corner-tl-${scanCycle}`}
218                    className={cn(
219                      "absolute top-0 left-0 w-6 h-6 pointer-events-none",
220                      "border-t-2 border-l-2",
221                      selectedColor.border
222                    )}
223                    initial={{ opacity: 0 }}
224                    animate={{ opacity: 1 }}
225                    exit={{ opacity: 0 }}
226                    transition={{ duration: 0.3 }}
227                  />
228                  <motion.div
229                    key={`corner-tr-${scanCycle}`}
230                    className={cn(
231                      "absolute top-0 right-0 w-6 h-6 pointer-events-none",
232                      "border-t-2 border-r-2",
233                      selectedColor.border
234                    )}
235                    initial={{ opacity: 0 }}
236                    animate={{ opacity: 1 }}
237                    exit={{ opacity: 0 }}
238                    transition={{ duration: 0.3 }}
239                  />
240                  <motion.div
241                    key={`corner-bl-${scanCycle}`}
242                    className={cn(
243                      "absolute bottom-0 left-0 w-6 h-6 pointer-events-none",
244                      "border-b-2 border-l-2",
245                      selectedColor.border
246                    )}
247                    initial={{ opacity: 0 }}
248                    animate={{ opacity: 1 }}
249                    exit={{ opacity: 0 }}
250                    transition={{ duration: 0.3 }}
251                  />
252                  <motion.div
253                    key={`corner-br-${scanCycle}`}
254                    className={cn(
255                      "absolute bottom-0 right-0 w-6 h-6 pointer-events-none",
256                      "border-b-2 border-r-2",
257                      selectedColor.border
258                    )}
259                    initial={{ opacity: 0 }}
260                    animate={{ opacity: 1 }}
261                    exit={{ opacity: 0 }}
262                    transition={{ duration: 0.3 }}
263                  />
264                </>
265              )}
266              <motion.div
267                key={`glow-${scanCycle}`}
268                className={cn(
269                  "absolute inset-0 pointer-events-none",
270                  selectedColor.glow
271                )}
272                initial={{ opacity: 0 }}
273                animate={{
274                  opacity: [0, 0.5, 0],
275                  transition: {
276                    repeat: 0,
277                    duration: scanSpeed / 2,
278                    repeatType: "reverse",
279                  },
280                }}
281                exit={{ opacity: 0 }}
282              />
283            </>
284          )}
285        </AnimatePresence>
286        <AnimatePresence>
287          {scanComplete && (
288            <motion.div
289              className={cn(
290                "absolute inset-0 pointer-events-none",
291                selectedColor.glow
292              )}
293              initial={{ opacity: 0 }}
294              animate={{ opacity: 0.7 }}
295              exit={{ opacity: 0 }}
296              transition={{ duration: 0.3 }}
297            />
298          )}
299        </AnimatePresence>
300      </div>
301    </div>
302  );
303};
304
4

Final Steps

Update Import Paths

Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button to match your UI components location.

Props

NameTypeDefaultDescription
imagestringundefinedURL or path to the image to be scanned.
altstring"Scanning image"Alternative text for the image.
scanDirection"horizontal" | "vertical""horizontal"Direction of the scanning effect.
scanSpeednumber2Speed of the scanning animation in seconds.
scanColor"emerald" | "blue" | "purple" | "amber" | "red""emerald"Color of the scanning effect.
scanType"line" | "corners" | "both""both"Type of scanning visual effect to display.
classNamestringundefinedAdditional CSS classes to apply to the container.
onScanComplete() => voidundefinedCallback function triggered when scanning is complete.
autoScanbooleanfalseAutomatically start scanning when component mounts.
scanDelaynumber0Delay in seconds before starting the auto scan.
scanAtScrollbooleanfalseTrigger scan when component is scrolled into view.
repeatingbooleanfalseEnable repeated scanning in a loop.
triggerScanbooleanfalseExternally trigger the scanning effect.

Examples

Repeating

Scanning image

Scan on scroll

Scanning image