UNPKG

@arolariu/components

Version:

🎨 70+ beautiful, accessible React components built on Base UI. TypeScript-first, CSS Modules styling, tree-shakeable, SSR-ready. Perfect for modern web apps, design systems & rapid prototyping. Zero config, maximum flexibility! ⚡

146 lines (145 loc) • 5.79 kB
"use client"; import { jsx, jsxs } from "react/jsx-runtime"; import { motion, useAnimation } from "motion/react"; import react, { useEffect, useRef, useState } from "react"; import { cn } from "../../lib/utilities.js"; import scratcher_module from "./scratcher.module.js"; const defaultGradientColors = [ "#A97CF8", "#F38CB8", "#FDCC92" ]; const ignoreAnimationError = ()=>null; const Scratcher = /*#__PURE__*/ react.forwardRef(({ width, height, minScratchPercentage = 50, onComplete, children, className, gradientColors = defaultGradientColors }, forwardedRef)=>{ const canvasRef = useRef(null); const [isScratching, setIsScratching] = useState(false); const [isComplete, setIsComplete] = useState(false); const controls = useAnimation(); useEffect(()=>{ const canvas = canvasRef.current; const context = canvas?.getContext("2d"); if (!canvas || !context) return; context.fillStyle = "#ccc"; context.fillRect(0, 0, canvas.width, canvas.height); const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height); gradient.addColorStop(0, gradientColors[0]); gradient.addColorStop(0.5, gradientColors[1]); gradient.addColorStop(1, gradientColors[2]); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); }, [ gradientColors ]); const scratch = react.useCallback((clientX, clientY)=>{ const canvas = canvasRef.current; const context = canvas?.getContext("2d"); if (!canvas || !context) return; const rect = canvas.getBoundingClientRect(); const x = clientX - rect.left + 16; const y = clientY - rect.top + 16; context.globalCompositeOperation = "destination-out"; context.beginPath(); context.arc(x, y, 30, 0, 2 * Math.PI); context.fill(); }, []); const startAnimation = react.useCallback(async ()=>{ await controls.start({ scale: [ 1, 1.5, 1 ], rotate: [ 0, 10, -10, 10, -10, 0 ], transition: { duration: 0.5 } }); onComplete?.(); }, [ controls, onComplete ]); const checkCompletion = react.useCallback(()=>{ if (isComplete) return; const canvas = canvasRef.current; const context = canvas?.getContext("2d"); if (!canvas || !context) return; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; const totalPixels = pixels.length / 4; let clearPixels = 0; for(let index = 3; index < pixels.length; index += 4)if (0 === pixels[index]) clearPixels += 1; const percentage = clearPixels / totalPixels * 100; if (percentage >= minScratchPercentage) { setIsComplete(true); context.clearRect(0, 0, canvas.width, canvas.height); startAnimation().catch(ignoreAnimationError); } }, [ isComplete, minScratchPercentage, startAnimation ]); useEffect(()=>{ const handleDocumentMouseMove = (event)=>{ if (isScratching) scratch(event.clientX, event.clientY); }; const handleDocumentTouchMove = (event)=>{ if (!isScratching) return; const [touch] = event.touches; if (!touch) return; scratch(touch.clientX, touch.clientY); }; const handleDocumentPointerEnd = ()=>{ setIsScratching(false); checkCompletion(); }; globalThis.document.addEventListener("mousemove", handleDocumentMouseMove); globalThis.document.addEventListener("touchmove", handleDocumentTouchMove); globalThis.document.addEventListener("mouseup", handleDocumentPointerEnd); globalThis.document.addEventListener("touchend", handleDocumentPointerEnd); globalThis.document.addEventListener("touchcancel", handleDocumentPointerEnd); return ()=>{ globalThis.document.removeEventListener("mousemove", handleDocumentMouseMove); globalThis.document.removeEventListener("touchmove", handleDocumentTouchMove); globalThis.document.removeEventListener("mouseup", handleDocumentPointerEnd); globalThis.document.removeEventListener("touchend", handleDocumentPointerEnd); globalThis.document.removeEventListener("touchcancel", handleDocumentPointerEnd); }; }, [ checkCompletion, isScratching, scratch ]); return /*#__PURE__*/ jsxs(motion.div, { ref: forwardedRef, className: cn(scratcher_module.root, className), style: { width, height, cursor: "url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNSIgc3R5bGU9ImZpbGw6I2ZmZjtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MXB4OyIgLz4KPC9zdmc+'), auto" }, animate: controls, children: [ /*#__PURE__*/ jsx("canvas", { ref: canvasRef, width: width, height: height, className: scratcher_module.canvas, onMouseDown: ()=>setIsScratching(true), onTouchStart: ()=>setIsScratching(true) }), children ] }); }); Scratcher.displayName = "Scratcher"; export { Scratcher }; //# sourceMappingURL=scratcher.js.map