{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-scratch-to-reveal",
  "files": [
    {
      "path": "components/ds/ds-scratch-to-reveal.tsx",
      "content": "\"use client\"\n\nimport React, { useRef, useState, useEffect } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n// ── Types ──\n\nexport interface ScratchToRevealProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Hidden content to be revealed */\n  children: React.ReactNode\n  /** Container width (px) */\n  width?: number\n  /** Container height (px) */\n  height?: number\n  /** Brush size for scratching */\n  brushSize?: number\n  /** Scratched percentage (0-100) to auto-reveal and trigger the callback */\n  revealThreshold?: number\n  /** Function called when the threshold is reached */\n  onReveal?: () => void\n  /** Hex or rgba color covering the canvas */\n  coverColor?: string\n}\n\n// ── Component ──\n\nexport const ScratchToReveal = React.forwardRef<\n  HTMLDivElement,\n  ScratchToRevealProps\n>(\n  (\n    {\n      children,\n      width = 300,\n      height = 150,\n      brushSize = 30,\n      revealThreshold = 50,\n      coverColor = \"#94a3b8\", // Default scratch color (slate-400)\n      onReveal,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    const canvasRef = useRef<HTMLCanvasElement>(null)\n    const [isRevealed, setIsRevealed] = useState(false)\n    const [isScratching, setIsScratching] = useState(false)\n    const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)\n\n    // Detect prefers-reduced-motion\n    useEffect(() => {\n      const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n      setPrefersReducedMotion(mediaQuery.matches)\n      if (mediaQuery.matches) {\n        setIsRevealed(true)\n        onReveal?.()\n      }\n    }, [onReveal])\n\n    // Draw the initial layer on the canvas\n    useEffect(() => {\n      if (prefersReducedMotion) return\n      const canvas = canvasRef.current\n      if (!canvas) return\n      const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })\n      if (!ctx) return\n\n      canvas.width = width\n      canvas.height = height\n\n      ctx.fillStyle = coverColor\n      ctx.fillRect(0, 0, width, height)\n\n      // Set the operation to \"erase\" anything drawn from now on\n      ctx.globalCompositeOperation = \"destination-out\"\n    }, [width, height, coverColor, prefersReducedMotion])\n\n    // Helper to calculate what % of the image has been \"erased\"\n    const calculateScratchedPercentage = React.useCallback(() => {\n      const canvas = canvasRef.current\n      if (!canvas) return 0\n      const ctx = canvas.getContext(\"2d\", { willReadFrequently: true })\n      if (!ctx) return 0\n\n      const pixels = ctx.getImageData(0, 0, width, height).data\n      let transparentPixels = 0\n      const totalPixels = width * height\n\n      // The array has 4 values (rgba) per pixel. We look at the alpha channel (index + 3)\n      for (let i = 0; i < pixels.length; i += 4) {\n        if (pixels[i + 3] === 0) {\n          transparentPixels++\n        }\n      }\n\n      return (transparentPixels / totalPixels) * 100\n    }, [width, height])\n\n    const handleReveal = React.useCallback(() => {\n      setIsRevealed(true)\n      onReveal?.()\n    }, [onReveal])\n\n    const checkReveal = React.useCallback(() => {\n      if (isRevealed) return\n      const percentage = calculateScratchedPercentage()\n      if (percentage > revealThreshold) {\n        handleReveal()\n      }\n    }, [\n      isRevealed,\n      revealThreshold,\n      handleReveal,\n      calculateScratchedPercentage,\n    ])\n\n    const scratch = React.useCallback(\n      (e: MouseEvent | TouchEvent) => {\n        if (!isScratching || isRevealed) return\n        const canvas = canvasRef.current\n        if (!canvas) return\n        const ctx = canvas.getContext(\"2d\")\n        if (!ctx) return\n\n        const rect = canvas.getBoundingClientRect()\n        const clientX = \"touches\" in e ? e.touches[0].clientX : e.clientX\n        const clientY = \"touches\" in e ? e.touches[0].clientY : e.clientY\n\n        const x = clientX - rect.left\n        const y = clientY - rect.top\n\n        ctx.beginPath()\n        ctx.arc(x, y, brushSize / 2, 0, 2 * Math.PI)\n        ctx.fill()\n      },\n      [isScratching, isRevealed, brushSize]\n    )\n\n    useEffect(() => {\n      const canvas = canvasRef.current\n      if (!canvas) return\n\n      const startScratch = (e: Event) => {\n        setIsScratching(true)\n        scratch(e as MouseEvent | TouchEvent)\n      }\n\n      const endScratch = () => {\n        setIsScratching(false)\n        checkReveal() // Check if enough has been scratched\n      }\n\n      const handleMove = (e: Event) => {\n        if (isScratching) {\n          e.preventDefault() // Prevent page scroll while scratching on mobile\n          scratch(e as MouseEvent | TouchEvent)\n        }\n      }\n\n      // Mouse support\n      canvas.addEventListener(\"mousedown\", startScratch)\n      canvas.addEventListener(\"mousemove\", handleMove, { passive: false })\n      window.addEventListener(\"mouseup\", endScratch)\n\n      // Touch support\n      canvas.addEventListener(\"touchstart\", startScratch, { passive: false })\n      canvas.addEventListener(\"touchmove\", handleMove, { passive: false })\n      window.addEventListener(\"touchend\", endScratch)\n\n      return () => {\n        canvas.removeEventListener(\"mousedown\", startScratch)\n        canvas.removeEventListener(\"mousemove\", handleMove)\n        window.removeEventListener(\"mouseup\", endScratch)\n        canvas.removeEventListener(\"touchstart\", startScratch)\n        canvas.removeEventListener(\"touchmove\", handleMove)\n        window.removeEventListener(\"touchend\", endScratch)\n      }\n    }, [isScratching, isRevealed, brushSize, checkReveal, scratch])\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"relative select-none\", className)}\n        style={{ width, height }}\n        {...props}\n      >\n        {/* Background layer (Secret) */}\n        <div className=\"absolute inset-0 z-0 flex items-center justify-center overflow-hidden\">\n          {children}\n        </div>\n\n        {/* Canvas layer (Scratch) */}\n        {!prefersReducedMotion && (\n          <canvas\n            ref={canvasRef}\n            className={cn(\n              \"absolute inset-0 z-10 touch-none transition-opacity duration-700\",\n              isRevealed ? \"pointer-events-none opacity-0\" : \"opacity-100\"\n            )}\n            style={{ width, height }}\n            aria-hidden=\"true\"\n          />\n        )}\n\n        {/* Fallback button for keyboard and screen readers */}\n        {!isRevealed && (\n          <button\n            onClick={handleReveal}\n            className=\"sr-only focus:not-sr-only focus:absolute focus:inset-0 focus:z-20 focus:flex focus:items-center focus:justify-center focus:bg-background/90 focus:text-sm focus:font-medium\"\n          >\n            Reveal secret content\n          </button>\n        )}\n      </div>\n    )\n  }\n)\n\nScratchToReveal.displayName = \"ScratchToReveal\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
