@arolariu/components 0.0.39 → 0.0.40

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.
Files changed (43) hide show
  1. package/CONTRIBUTING.md +371 -371
  2. package/DEBUGGING.md +401 -401
  3. package/EXAMPLES.md +1035 -1035
  4. package/{CHANGELOG.md → changelog.md} +7 -0
  5. package/dist/cjs/components/ui/bubble-background.cjs +1 -2
  6. package/dist/cjs/components/ui/bubble-background.cjs.map +1 -1
  7. package/dist/cjs/components/ui/calendar.cjs.map +1 -1
  8. package/dist/cjs/components/ui/chart.cjs.map +1 -1
  9. package/dist/cjs/components/ui/command.cjs +1 -1
  10. package/dist/cjs/components/ui/drawer.cjs.map +1 -1
  11. package/dist/cjs/components/ui/dropdrawer.cjs.map +1 -1
  12. package/dist/cjs/components/ui/input.cjs.map +1 -1
  13. package/dist/cjs/components/ui/ripple-button.cjs.map +1 -1
  14. package/dist/cjs/components/ui/scratcher.cjs.map +1 -1
  15. package/dist/cjs/components/ui/sidebar.cjs +4 -4
  16. package/dist/cjs/components/ui/sonner.cjs +2 -2
  17. package/dist/cjs/components/ui/tooltip.cjs +1 -1
  18. package/dist/cjs/index.cjs +6 -6
  19. package/dist/cjs/index.css +1 -1
  20. package/dist/cjs/index.css.map +1 -1
  21. package/dist/esm/components/ui/bubble-background.js +1 -2
  22. package/dist/esm/components/ui/bubble-background.js.map +1 -1
  23. package/dist/esm/components/ui/calendar.js.map +1 -1
  24. package/dist/esm/components/ui/chart.js.map +1 -1
  25. package/dist/esm/components/ui/drawer.js.map +1 -1
  26. package/dist/esm/components/ui/dropdrawer.js.map +1 -1
  27. package/dist/esm/components/ui/input.js.map +1 -1
  28. package/dist/esm/components/ui/ripple-button.js.map +1 -1
  29. package/dist/esm/components/ui/scratcher.js.map +1 -1
  30. package/dist/esm/index.css +1 -1
  31. package/dist/esm/index.css.map +1 -1
  32. package/dist/index.css +1 -1
  33. package/dist/types/components/ui/bubble-background.d.ts.map +1 -1
  34. package/package.json +51 -52
  35. package/{README.md → readme.md} +627 -627
  36. package/src/components/ui/bubble-background.tsx +189 -187
  37. package/src/components/ui/calendar.tsx +213 -213
  38. package/src/components/ui/chart.tsx +380 -380
  39. package/src/components/ui/drawer.tsx +141 -141
  40. package/src/components/ui/dropdrawer.tsx +973 -973
  41. package/src/components/ui/input.tsx +22 -22
  42. package/src/components/ui/ripple-button.tsx +111 -111
  43. package/src/components/ui/scratcher.tsx +171 -171
@@ -1,22 +1,22 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { cn } from "@/lib/utils";
5
-
6
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7
- return (
8
- <input
9
- type={type}
10
- data-slot="input"
11
- className={cn(
12
- "file:text-neutral-950 placeholder:text-neutral-500 selection:bg-neutral-900 selection:text-neutral-50 dark:bg-neutral-200/30 border-neutral-200 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:selection:bg-neutral-50 dark:selection:text-neutral-900 dark:dark:bg-neutral-800/30 dark:border-neutral-800",
13
- "focus-visible:border-neutral-950 focus-visible:ring-neutral-950/50 focus-visible:ring-[3px] dark:focus-visible:border-neutral-300 dark:focus-visible:ring-neutral-300/50",
14
- "aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40 aria-invalid:border-red-500 dark:aria-invalid:ring-red-900/20 dark:dark:aria-invalid:ring-red-900/40 dark:aria-invalid:border-red-900",
15
- className,
16
- )}
17
- {...props}
18
- />
19
- );
20
- }
21
-
22
- export { Input };
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7
+ return (
8
+ <input
9
+ type={type}
10
+ data-slot="input"
11
+ className={cn(
12
+ "file:text-neutral-950 placeholder:text-neutral-500 selection:bg-neutral-900 selection:text-neutral-50 dark:bg-neutral-200/30 border-neutral-200 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:selection:bg-neutral-50 dark:selection:text-neutral-900 dark:dark:bg-neutral-800/30 dark:border-neutral-800",
13
+ "focus-visible:border-neutral-950 focus-visible:ring-neutral-950/50 focus-visible:ring-[3px] dark:focus-visible:border-neutral-300 dark:focus-visible:ring-neutral-300/50",
14
+ "aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40 aria-invalid:border-red-500 dark:aria-invalid:ring-red-900/20 dark:dark:aria-invalid:ring-red-900/40 dark:aria-invalid:border-red-900",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ export { Input };
@@ -1,111 +1,111 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { type HTMLMotionProps, motion, type Transition } from "motion/react";
5
-
6
- import { cn } from "@/lib/utils";
7
-
8
- interface Ripple {
9
- id: number;
10
- x: number;
11
- y: number;
12
- }
13
-
14
- interface RippleButtonProps extends HTMLMotionProps<"button"> {
15
- children: React.ReactNode;
16
- rippleClassName?: string;
17
- scale?: number;
18
- transition?: Transition;
19
- }
20
-
21
- const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
22
- (
23
- {
24
- children,
25
- onClick,
26
- className,
27
- rippleClassName,
28
- scale = 10,
29
- transition = { duration: 0.6, ease: "easeOut" },
30
- ...props
31
- },
32
- ref,
33
- ) => {
34
- const [ripples, setRipples] = React.useState<Ripple[]>([]);
35
- const buttonRef = React.useRef<HTMLButtonElement>(null);
36
- React.useImperativeHandle(
37
- ref,
38
- () => buttonRef.current as HTMLButtonElement,
39
- );
40
-
41
- const createRipple = React.useCallback(
42
- (event: React.MouseEvent<HTMLButtonElement>) => {
43
- const button = buttonRef.current;
44
- if (!button) return;
45
-
46
- const rect = button.getBoundingClientRect();
47
- const x = event.clientX - rect.left;
48
- const y = event.clientY - rect.top;
49
-
50
- const newRipple: Ripple = {
51
- id: Date.now(),
52
- x,
53
- y,
54
- };
55
-
56
- setRipples((prev) => [...prev, newRipple]);
57
-
58
- setTimeout(() => {
59
- setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
60
- }, 600);
61
- },
62
- [],
63
- );
64
-
65
- const handleClick = React.useCallback(
66
- (event: React.MouseEvent<HTMLButtonElement>) => {
67
- createRipple(event);
68
- if (onClick) {
69
- onClick(event);
70
- }
71
- },
72
- [createRipple, onClick],
73
- );
74
-
75
- return (
76
- <motion.button
77
- ref={buttonRef}
78
- onClick={handleClick}
79
- whileTap={{ scale: 0.95 }}
80
- whileHover={{ scale: 1.05 }}
81
- className={cn(
82
- "relative h-10 px-4 py-2 text-sm font-medium text-primary-foreground overflow-hidden bg-primary cursor-pointer rounded-lg focus:outline-none",
83
- className,
84
- )}
85
- {...props}
86
- >
87
- {children}
88
- {ripples.map((ripple) => (
89
- <motion.span
90
- key={ripple.id}
91
- initial={{ scale: 0, opacity: 0.5 }}
92
- animate={{ scale, opacity: 0 }}
93
- transition={transition}
94
- className={cn(
95
- "absolute bg-primary-foreground rounded-full size-5 pointer-events-none",
96
- rippleClassName,
97
- )}
98
- style={{
99
- top: ripple.y - 10,
100
- left: ripple.x - 10,
101
- }}
102
- />
103
- ))}
104
- </motion.button>
105
- );
106
- },
107
- );
108
-
109
- RippleButton.displayName = "RippleButton";
110
-
111
- export { RippleButton, type RippleButtonProps };
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { type HTMLMotionProps, motion, type Transition } from "motion/react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface Ripple {
9
+ id: number;
10
+ x: number;
11
+ y: number;
12
+ }
13
+
14
+ interface RippleButtonProps extends HTMLMotionProps<"button"> {
15
+ children: React.ReactNode;
16
+ rippleClassName?: string;
17
+ scale?: number;
18
+ transition?: Transition;
19
+ }
20
+
21
+ const RippleButton = React.forwardRef<HTMLButtonElement, RippleButtonProps>(
22
+ (
23
+ {
24
+ children,
25
+ onClick,
26
+ className,
27
+ rippleClassName,
28
+ scale = 10,
29
+ transition = { duration: 0.6, ease: "easeOut" },
30
+ ...props
31
+ },
32
+ ref,
33
+ ) => {
34
+ const [ripples, setRipples] = React.useState<Ripple[]>([]);
35
+ const buttonRef = React.useRef<HTMLButtonElement>(null);
36
+ React.useImperativeHandle(
37
+ ref,
38
+ () => buttonRef.current as HTMLButtonElement,
39
+ );
40
+
41
+ const createRipple = React.useCallback(
42
+ (event: React.MouseEvent<HTMLButtonElement>) => {
43
+ const button = buttonRef.current;
44
+ if (!button) return;
45
+
46
+ const rect = button.getBoundingClientRect();
47
+ const x = event.clientX - rect.left;
48
+ const y = event.clientY - rect.top;
49
+
50
+ const newRipple: Ripple = {
51
+ id: Date.now(),
52
+ x,
53
+ y,
54
+ };
55
+
56
+ setRipples((prev) => [...prev, newRipple]);
57
+
58
+ setTimeout(() => {
59
+ setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
60
+ }, 600);
61
+ },
62
+ [],
63
+ );
64
+
65
+ const handleClick = React.useCallback(
66
+ (event: React.MouseEvent<HTMLButtonElement>) => {
67
+ createRipple(event);
68
+ if (onClick) {
69
+ onClick(event);
70
+ }
71
+ },
72
+ [createRipple, onClick],
73
+ );
74
+
75
+ return (
76
+ <motion.button
77
+ ref={buttonRef}
78
+ onClick={handleClick}
79
+ whileTap={{ scale: 0.95 }}
80
+ whileHover={{ scale: 1.05 }}
81
+ className={cn(
82
+ "relative h-10 px-4 py-2 text-sm font-medium text-primary-foreground overflow-hidden bg-primary cursor-pointer rounded-lg focus:outline-none",
83
+ className,
84
+ )}
85
+ {...props}
86
+ >
87
+ {children}
88
+ {ripples.map((ripple) => (
89
+ <motion.span
90
+ key={ripple.id}
91
+ initial={{ scale: 0, opacity: 0.5 }}
92
+ animate={{ scale, opacity: 0 }}
93
+ transition={transition}
94
+ className={cn(
95
+ "absolute bg-primary-foreground rounded-full size-5 pointer-events-none",
96
+ rippleClassName,
97
+ )}
98
+ style={{
99
+ top: ripple.y - 10,
100
+ left: ripple.x - 10,
101
+ }}
102
+ />
103
+ ))}
104
+ </motion.button>
105
+ );
106
+ },
107
+ );
108
+
109
+ RippleButton.displayName = "RippleButton";
110
+
111
+ export { RippleButton, type RippleButtonProps };
@@ -1,171 +1,171 @@
1
- "use client";
2
-
3
- import { cn } from "@/lib/utils";
4
- import { motion, useAnimation } from "motion/react";
5
- import React, { useEffect, useRef, useState } from "react";
6
-
7
- interface ScratcherProps {
8
- children: React.ReactNode;
9
- width: number;
10
- height: number;
11
- minScratchPercentage?: number;
12
- className?: string;
13
- onComplete?: () => void;
14
- gradientColors?: [string, string, string];
15
- }
16
-
17
- export const Scratcher: React.FC<ScratcherProps> = ({
18
- width,
19
- height,
20
- minScratchPercentage = 50,
21
- onComplete,
22
- children,
23
- className,
24
- gradientColors = ["#A97CF8", "#F38CB8", "#FDCC92"],
25
- }) => {
26
- const canvasRef = useRef<HTMLCanvasElement>(null);
27
- const [isScratching, setIsScratching] = useState(false);
28
- const [isComplete, setIsComplete] = useState(false);
29
-
30
- const controls = useAnimation();
31
-
32
- useEffect(() => {
33
- const canvas = canvasRef.current;
34
- const ctx = canvas?.getContext("2d");
35
- if (canvas && ctx) {
36
- ctx.fillStyle = "#ccc";
37
- ctx.fillRect(0, 0, canvas.width, canvas.height);
38
- const gradient = ctx.createLinearGradient(
39
- 0,
40
- 0,
41
- canvas.width,
42
- canvas.height,
43
- );
44
- gradient.addColorStop(0, gradientColors[0]);
45
- gradient.addColorStop(0.5, gradientColors[1]);
46
- gradient.addColorStop(1, gradientColors[2]);
47
- ctx.fillStyle = gradient;
48
- ctx.fillRect(0, 0, canvas.width, canvas.height);
49
- }
50
- }, [gradientColors]);
51
-
52
- useEffect(() => {
53
- const handleDocumentMouseMove = (event: MouseEvent) => {
54
- if (!isScratching) return;
55
- scratch(event.clientX, event.clientY);
56
- };
57
-
58
- const handleDocumentTouchMove = (event: TouchEvent) => {
59
- if (!isScratching) return;
60
- const touch = event.touches[0];
61
- scratch(touch.clientX, touch.clientY);
62
- };
63
-
64
- const handleDocumentMouseUp = () => {
65
- setIsScratching(false);
66
- checkCompletion();
67
- };
68
-
69
- const handleDocumentTouchEnd = () => {
70
- setIsScratching(false);
71
- checkCompletion();
72
- };
73
-
74
- document.addEventListener("mousedown", handleDocumentMouseMove);
75
- document.addEventListener("mousemove", handleDocumentMouseMove);
76
- document.addEventListener("touchstart", handleDocumentTouchMove);
77
- document.addEventListener("touchmove", handleDocumentTouchMove);
78
- document.addEventListener("mouseup", handleDocumentMouseUp);
79
- document.addEventListener("touchend", handleDocumentTouchEnd);
80
- document.addEventListener("touchcancel", handleDocumentTouchEnd);
81
-
82
- return () => {
83
- document.removeEventListener("mousedown", handleDocumentMouseMove);
84
- document.removeEventListener("mousemove", handleDocumentMouseMove);
85
- document.removeEventListener("touchstart", handleDocumentTouchMove);
86
- document.removeEventListener("touchmove", handleDocumentTouchMove);
87
- document.removeEventListener("mouseup", handleDocumentMouseUp);
88
- document.removeEventListener("touchend", handleDocumentTouchEnd);
89
- document.removeEventListener("touchcancel", handleDocumentTouchEnd);
90
- };
91
- }, [isScratching]);
92
-
93
- const handleMouseDown = () => setIsScratching(true);
94
-
95
- const handleTouchStart = () => setIsScratching(true);
96
-
97
- const scratch = (clientX: number, clientY: number) => {
98
- const canvas = canvasRef.current;
99
- const ctx = canvas?.getContext("2d");
100
- if (canvas && ctx) {
101
- const rect = canvas.getBoundingClientRect();
102
- const x = clientX - rect.left + 16;
103
- const y = clientY - rect.top + 16;
104
- ctx.globalCompositeOperation = "destination-out";
105
- ctx.beginPath();
106
- ctx.arc(x, y, 30, 0, Math.PI * 2);
107
- ctx.fill();
108
- }
109
- };
110
-
111
- const startAnimation = async () => {
112
- await controls.start({
113
- scale: [1, 1.5, 1],
114
- rotate: [0, 10, -10, 10, -10, 0],
115
- transition: { duration: 0.5 },
116
- });
117
-
118
- // Call onComplete after animation finishes
119
- if (onComplete) {
120
- onComplete();
121
- }
122
- };
123
-
124
- const checkCompletion = () => {
125
- if (isComplete) return;
126
-
127
- const canvas = canvasRef.current;
128
- const ctx = canvas?.getContext("2d");
129
- if (canvas && ctx) {
130
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
131
- const pixels = imageData.data;
132
- const totalPixels = pixels.length / 4;
133
- let clearPixels = 0;
134
-
135
- for (let i = 3; i < pixels.length; i += 4) {
136
- if (pixels[i] === 0) clearPixels++;
137
- }
138
-
139
- const percentage = (clearPixels / totalPixels) * 100;
140
-
141
- if (percentage >= minScratchPercentage) {
142
- setIsComplete(true);
143
- ctx.clearRect(0, 0, canvas.width, canvas.height);
144
- startAnimation();
145
- }
146
- }
147
- };
148
-
149
- return (
150
- <motion.div
151
- className={cn("relative select-none", className)}
152
- style={{
153
- width,
154
- height,
155
- cursor:
156
- "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto",
157
- }}
158
- animate={controls}
159
- >
160
- <canvas
161
- ref={canvasRef}
162
- width={width}
163
- height={height}
164
- className="absolute left-0 top-0"
165
- onMouseDown={handleMouseDown}
166
- onTouchStart={handleTouchStart}
167
- ></canvas>
168
- {children}
169
- </motion.div>
170
- );
171
- };
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { motion, useAnimation } from "motion/react";
5
+ import React, { useEffect, useRef, useState } from "react";
6
+
7
+ interface ScratcherProps {
8
+ children: React.ReactNode;
9
+ width: number;
10
+ height: number;
11
+ minScratchPercentage?: number;
12
+ className?: string;
13
+ onComplete?: () => void;
14
+ gradientColors?: [string, string, string];
15
+ }
16
+
17
+ export const Scratcher: React.FC<ScratcherProps> = ({
18
+ width,
19
+ height,
20
+ minScratchPercentage = 50,
21
+ onComplete,
22
+ children,
23
+ className,
24
+ gradientColors = ["#A97CF8", "#F38CB8", "#FDCC92"],
25
+ }) => {
26
+ const canvasRef = useRef<HTMLCanvasElement>(null);
27
+ const [isScratching, setIsScratching] = useState(false);
28
+ const [isComplete, setIsComplete] = useState(false);
29
+
30
+ const controls = useAnimation();
31
+
32
+ useEffect(() => {
33
+ const canvas = canvasRef.current;
34
+ const ctx = canvas?.getContext("2d");
35
+ if (canvas && ctx) {
36
+ ctx.fillStyle = "#ccc";
37
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
38
+ const gradient = ctx.createLinearGradient(
39
+ 0,
40
+ 0,
41
+ canvas.width,
42
+ canvas.height,
43
+ );
44
+ gradient.addColorStop(0, gradientColors[0]);
45
+ gradient.addColorStop(0.5, gradientColors[1]);
46
+ gradient.addColorStop(1, gradientColors[2]);
47
+ ctx.fillStyle = gradient;
48
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
49
+ }
50
+ }, [gradientColors]);
51
+
52
+ useEffect(() => {
53
+ const handleDocumentMouseMove = (event: MouseEvent) => {
54
+ if (!isScratching) return;
55
+ scratch(event.clientX, event.clientY);
56
+ };
57
+
58
+ const handleDocumentTouchMove = (event: TouchEvent) => {
59
+ if (!isScratching) return;
60
+ const touch = event.touches[0];
61
+ scratch(touch.clientX, touch.clientY);
62
+ };
63
+
64
+ const handleDocumentMouseUp = () => {
65
+ setIsScratching(false);
66
+ checkCompletion();
67
+ };
68
+
69
+ const handleDocumentTouchEnd = () => {
70
+ setIsScratching(false);
71
+ checkCompletion();
72
+ };
73
+
74
+ document.addEventListener("mousedown", handleDocumentMouseMove);
75
+ document.addEventListener("mousemove", handleDocumentMouseMove);
76
+ document.addEventListener("touchstart", handleDocumentTouchMove);
77
+ document.addEventListener("touchmove", handleDocumentTouchMove);
78
+ document.addEventListener("mouseup", handleDocumentMouseUp);
79
+ document.addEventListener("touchend", handleDocumentTouchEnd);
80
+ document.addEventListener("touchcancel", handleDocumentTouchEnd);
81
+
82
+ return () => {
83
+ document.removeEventListener("mousedown", handleDocumentMouseMove);
84
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
85
+ document.removeEventListener("touchstart", handleDocumentTouchMove);
86
+ document.removeEventListener("touchmove", handleDocumentTouchMove);
87
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
88
+ document.removeEventListener("touchend", handleDocumentTouchEnd);
89
+ document.removeEventListener("touchcancel", handleDocumentTouchEnd);
90
+ };
91
+ }, [isScratching]);
92
+
93
+ const handleMouseDown = () => setIsScratching(true);
94
+
95
+ const handleTouchStart = () => setIsScratching(true);
96
+
97
+ const scratch = (clientX: number, clientY: number) => {
98
+ const canvas = canvasRef.current;
99
+ const ctx = canvas?.getContext("2d");
100
+ if (canvas && ctx) {
101
+ const rect = canvas.getBoundingClientRect();
102
+ const x = clientX - rect.left + 16;
103
+ const y = clientY - rect.top + 16;
104
+ ctx.globalCompositeOperation = "destination-out";
105
+ ctx.beginPath();
106
+ ctx.arc(x, y, 30, 0, Math.PI * 2);
107
+ ctx.fill();
108
+ }
109
+ };
110
+
111
+ const startAnimation = async () => {
112
+ await controls.start({
113
+ scale: [1, 1.5, 1],
114
+ rotate: [0, 10, -10, 10, -10, 0],
115
+ transition: { duration: 0.5 },
116
+ });
117
+
118
+ // Call onComplete after animation finishes
119
+ if (onComplete) {
120
+ onComplete();
121
+ }
122
+ };
123
+
124
+ const checkCompletion = () => {
125
+ if (isComplete) return;
126
+
127
+ const canvas = canvasRef.current;
128
+ const ctx = canvas?.getContext("2d");
129
+ if (canvas && ctx) {
130
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
131
+ const pixels = imageData.data;
132
+ const totalPixels = pixels.length / 4;
133
+ let clearPixels = 0;
134
+
135
+ for (let i = 3; i < pixels.length; i += 4) {
136
+ if (pixels[i] === 0) clearPixels++;
137
+ }
138
+
139
+ const percentage = (clearPixels / totalPixels) * 100;
140
+
141
+ if (percentage >= minScratchPercentage) {
142
+ setIsComplete(true);
143
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
144
+ startAnimation();
145
+ }
146
+ }
147
+ };
148
+
149
+ return (
150
+ <motion.div
151
+ className={cn("relative select-none", className)}
152
+ style={{
153
+ width,
154
+ height,
155
+ cursor:
156
+ "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto",
157
+ }}
158
+ animate={controls}
159
+ >
160
+ <canvas
161
+ ref={canvasRef}
162
+ width={width}
163
+ height={height}
164
+ className="absolute left-0 top-0"
165
+ onMouseDown={handleMouseDown}
166
+ onTouchStart={handleTouchStart}
167
+ ></canvas>
168
+ {children}
169
+ </motion.div>
170
+ );
171
+ };