@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.
- package/CONTRIBUTING.md +371 -371
- package/DEBUGGING.md +401 -401
- package/EXAMPLES.md +1035 -1035
- package/{CHANGELOG.md → changelog.md} +7 -0
- package/dist/cjs/components/ui/bubble-background.cjs +1 -2
- package/dist/cjs/components/ui/bubble-background.cjs.map +1 -1
- package/dist/cjs/components/ui/calendar.cjs.map +1 -1
- package/dist/cjs/components/ui/chart.cjs.map +1 -1
- package/dist/cjs/components/ui/command.cjs +1 -1
- package/dist/cjs/components/ui/drawer.cjs.map +1 -1
- package/dist/cjs/components/ui/dropdrawer.cjs.map +1 -1
- package/dist/cjs/components/ui/input.cjs.map +1 -1
- package/dist/cjs/components/ui/ripple-button.cjs.map +1 -1
- package/dist/cjs/components/ui/scratcher.cjs.map +1 -1
- package/dist/cjs/components/ui/sidebar.cjs +4 -4
- package/dist/cjs/components/ui/sonner.cjs +2 -2
- package/dist/cjs/components/ui/tooltip.cjs +1 -1
- package/dist/cjs/index.cjs +6 -6
- package/dist/cjs/index.css +1 -1
- package/dist/cjs/index.css.map +1 -1
- package/dist/esm/components/ui/bubble-background.js +1 -2
- package/dist/esm/components/ui/bubble-background.js.map +1 -1
- package/dist/esm/components/ui/calendar.js.map +1 -1
- package/dist/esm/components/ui/chart.js.map +1 -1
- package/dist/esm/components/ui/drawer.js.map +1 -1
- package/dist/esm/components/ui/dropdrawer.js.map +1 -1
- package/dist/esm/components/ui/input.js.map +1 -1
- package/dist/esm/components/ui/ripple-button.js.map +1 -1
- package/dist/esm/components/ui/scratcher.js.map +1 -1
- package/dist/esm/index.css +1 -1
- package/dist/esm/index.css.map +1 -1
- package/dist/index.css +1 -1
- package/dist/types/components/ui/bubble-background.d.ts.map +1 -1
- package/package.json +51 -52
- package/{README.md → readme.md} +627 -627
- package/src/components/ui/bubble-background.tsx +189 -187
- package/src/components/ui/calendar.tsx +213 -213
- package/src/components/ui/chart.tsx +380 -380
- package/src/components/ui/drawer.tsx +141 -141
- package/src/components/ui/dropdrawer.tsx +973 -973
- package/src/components/ui/input.tsx +22 -22
- package/src/components/ui/ripple-button.tsx +111 -111
- 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
|
+
};
|