{
  "name": "webcam-pixel-grid",
  "type": "registry:ui",
  "files": [
    {
      "path": "components/ui/webcam-pixel-grid.tsx",
      "content": "\"use client\";\nimport React, { useRef, useEffect, useState, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype WebcamPixelGridProps = {\n  /** Number of columns in the grid */\n  gridCols?: number;\n  /** Number of rows in the grid */\n  gridRows?: number;\n  /** Maximum elevation for motion detection */\n  maxElevation?: number;\n  /** Motion sensitivity (0-1) */\n  motionSensitivity?: number;\n  /** Smoothing factor for elevation transitions */\n  elevationSmoothing?: number;\n  /** Color mode: 'webcam' uses actual colors, 'monochrome' uses single color */\n  colorMode?: \"webcam\" | \"monochrome\";\n  /** Base color when in monochrome mode */\n  monochromeColor?: string;\n  /** Background color */\n  backgroundColor?: string;\n  /** Whether to mirror the webcam feed */\n  mirror?: boolean;\n  /** Gap between cells (0-1, fraction of cell size) */\n  gapRatio?: number;\n  /** Invert the colors */\n  invertColors?: boolean;\n  /** Darken factor (0-1, 0 = no darkening, 1 = fully dark) */\n  darken?: number;\n  /** Border color for cells */\n  borderColor?: string;\n  /** Border opacity (0-1) */\n  borderOpacity?: number;\n  /** Additional class name */\n  className?: string;\n  /** Callback when webcam access is denied */\n  onWebcamError?: (error: Error) => void;\n  /** Callback when webcam is ready */\n  onWebcamReady?: () => void;\n};\n\ntype PixelData = {\n  r: number;\n  g: number;\n  b: number;\n  motion: number;\n  targetElevation: number;\n  currentElevation: number;\n};\n\nexport const WebcamPixelGrid: React.FC<WebcamPixelGridProps> = ({\n  gridCols = 64,\n  gridRows = 48,\n  maxElevation = 15,\n  motionSensitivity = 0.4,\n  elevationSmoothing = 0.1,\n  colorMode = \"webcam\",\n  monochromeColor = \"#00ff88\",\n  backgroundColor = \"#0a0a0a\",\n  mirror = true,\n  gapRatio = 0.1,\n  invertColors = false,\n  darken = 0,\n  borderColor = \"#ffffff\",\n  borderOpacity = 0.08,\n  className,\n  onWebcamError,\n  onWebcamReady,\n}) => {\n  const videoRef = useRef<HTMLVideoElement>(null);\n  const processingCanvasRef = useRef<HTMLCanvasElement>(null);\n  const displayCanvasRef = useRef<HTMLCanvasElement>(null);\n  const previousFrameRef = useRef<Uint8ClampedArray | null>(null);\n  const pixelDataRef = useRef<PixelData[][]>([]);\n  const animationRef = useRef<number>(0);\n  const [isReady, setIsReady] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [showErrorPopup, setShowErrorPopup] = useState(true);\n\n  // Parse monochrome color\n  const monoRGB = React.useMemo(() => {\n    const hex = monochromeColor.replace(\"#\", \"\");\n    return {\n      r: parseInt(hex.slice(0, 2), 16),\n      g: parseInt(hex.slice(2, 4), 16),\n      b: parseInt(hex.slice(4, 6), 16),\n    };\n  }, [monochromeColor]);\n\n  // Parse border color\n  const borderRGB = React.useMemo(() => {\n    const hex = borderColor.replace(\"#\", \"\");\n    return {\n      r: parseInt(hex.slice(0, 2), 16),\n      g: parseInt(hex.slice(2, 4), 16),\n      b: parseInt(hex.slice(4, 6), 16),\n    };\n  }, [borderColor]);\n\n  // Initialize pixel data\n  useEffect(() => {\n    pixelDataRef.current = Array.from({ length: gridRows }, () =>\n      Array.from({ length: gridCols }, () => ({\n        r: 30,\n        g: 30,\n        b: 30,\n        motion: 0,\n        targetElevation: 0,\n        currentElevation: 0,\n      })),\n    );\n  }, [gridCols, gridRows]);\n\n  const streamRef = useRef<MediaStream | null>(null);\n\n  // Request camera access\n  const requestCameraAccess = useCallback(async () => {\n    try {\n      const stream = await navigator.mediaDevices.getUserMedia({\n        video: {\n          width: { ideal: 640 },\n          height: { ideal: 480 },\n          facingMode: \"user\",\n        },\n      });\n\n      streamRef.current = stream;\n\n      if (videoRef.current) {\n        videoRef.current.srcObject = stream;\n        await videoRef.current.play();\n        setIsReady(true);\n        setError(null);\n        setShowErrorPopup(false);\n        onWebcamReady?.();\n      }\n    } catch (err) {\n      const error =\n        err instanceof Error ? err : new Error(\"Webcam access denied\");\n      setError(error.message);\n      onWebcamError?.(error);\n    }\n  }, [onWebcamError, onWebcamReady]);\n\n  // Initialize webcam on mount\n  useEffect(() => {\n    requestCameraAccess();\n\n    return () => {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop());\n      }\n    };\n  }, [requestCameraAccess]);\n\n  // Main render loop\n  const render = useCallback(() => {\n    const video = videoRef.current;\n    const processingCanvas = processingCanvasRef.current;\n    const displayCanvas = displayCanvasRef.current;\n\n    if (!video || !processingCanvas || !displayCanvas || video.readyState < 2) {\n      animationRef.current = requestAnimationFrame(render);\n      return;\n    }\n\n    const procCtx = processingCanvas.getContext(\"2d\", {\n      willReadFrequently: true,\n    });\n    const dispCtx = displayCanvas.getContext(\"2d\");\n\n    if (!procCtx || !dispCtx) {\n      animationRef.current = requestAnimationFrame(render);\n      return;\n    }\n\n    // Set processing canvas size to grid dimensions\n    processingCanvas.width = gridCols;\n    processingCanvas.height = gridRows;\n\n    // Draw video to processing canvas (scaled down)\n    procCtx.save();\n    if (mirror) {\n      procCtx.scale(-1, 1);\n      procCtx.drawImage(video, -gridCols, 0, gridCols, gridRows);\n    } else {\n      procCtx.drawImage(video, 0, 0, gridCols, gridRows);\n    }\n    procCtx.restore();\n\n    // Get pixel data\n    const imageData = procCtx.getImageData(0, 0, gridCols, gridRows);\n    const currentData = imageData.data;\n    const previousData = previousFrameRef.current;\n\n    // Update pixel data with motion detection\n    const pixels = pixelDataRef.current;\n    for (let row = 0; row < gridRows; row++) {\n      for (let col = 0; col < gridCols; col++) {\n        const idx = (row * gridCols + col) * 4;\n        const r = currentData[idx];\n        const g = currentData[idx + 1];\n        const b = currentData[idx + 2];\n\n        const pixel = pixels[row]?.[col];\n        if (!pixel) continue;\n\n        // Calculate motion\n        let motion = 0;\n        if (previousData) {\n          const prevR = previousData[idx];\n          const prevG = previousData[idx + 1];\n          const prevB = previousData[idx + 2];\n          const diff =\n            Math.abs(r - prevR) + Math.abs(g - prevG) + Math.abs(b - prevB);\n          motion = Math.min(1, diff / 255 / motionSensitivity);\n        }\n\n        // Smooth motion\n        pixel.motion = pixel.motion * 0.7 + motion * 0.3;\n\n        // Set colors\n        let finalR = r;\n        let finalG = g;\n        let finalB = b;\n\n        if (colorMode === \"monochrome\") {\n          const brightness = (r + g + b) / 3 / 255;\n          finalR = Math.round(monoRGB.r * brightness);\n          finalG = Math.round(monoRGB.g * brightness);\n          finalB = Math.round(monoRGB.b * brightness);\n        }\n\n        // Apply invert\n        if (invertColors) {\n          finalR = 255 - finalR;\n          finalG = 255 - finalG;\n          finalB = 255 - finalB;\n        }\n\n        // Apply darken\n        if (darken > 0) {\n          const darkenFactor = 1 - darken;\n          finalR = Math.round(finalR * darkenFactor);\n          finalG = Math.round(finalG * darkenFactor);\n          finalB = Math.round(finalB * darkenFactor);\n        }\n\n        pixel.r = finalR;\n        pixel.g = finalG;\n        pixel.b = finalB;\n\n        // Set target elevation\n        pixel.targetElevation = pixel.motion * maxElevation;\n\n        // Smooth elevation transition\n        pixel.currentElevation +=\n          (pixel.targetElevation - pixel.currentElevation) * elevationSmoothing;\n      }\n    }\n\n    // Store current frame for next comparison\n    previousFrameRef.current = new Uint8ClampedArray(currentData);\n\n    // Render to display canvas\n    const dpr = window.devicePixelRatio || 1;\n    const displayWidth = displayCanvas.clientWidth;\n    const displayHeight = displayCanvas.clientHeight;\n\n    displayCanvas.width = displayWidth * dpr;\n    displayCanvas.height = displayHeight * dpr;\n    dispCtx.scale(dpr, dpr);\n\n    // Clear canvas\n    dispCtx.fillStyle = backgroundColor;\n    dispCtx.fillRect(0, 0, displayWidth, displayHeight);\n\n    // Calculate cell size (always square, cover entire container like object-fit: cover)\n    const cellSize = Math.max(\n      displayWidth / gridCols,\n      displayHeight / gridRows,\n    );\n    const gap = cellSize * gapRatio;\n\n    // Calculate offset to center the grid (negative offset for overflow, creating cover effect)\n    const gridWidth = cellSize * gridCols;\n    const gridHeight = cellSize * gridRows;\n    const offsetXGrid = (displayWidth - gridWidth) / 2;\n    const offsetYGrid = (displayHeight - gridHeight) / 2;\n\n    // Draw cells with 3D effect\n    for (let row = 0; row < gridRows; row++) {\n      for (let col = 0; col < gridCols; col++) {\n        const pixel = pixels[row]?.[col];\n        if (!pixel) continue;\n\n        const x = offsetXGrid + col * cellSize;\n        const y = offsetYGrid + row * cellSize;\n        const elevation = pixel.currentElevation;\n\n        // Calculate 3D offset (isometric-like projection) - MUCH larger effect\n        const offsetX = -elevation * 1.2;\n        const offsetY = -elevation * 1.8;\n\n        // Draw shadow - larger and more visible\n        if (elevation > 0.5) {\n          dispCtx.fillStyle = `rgba(0, 0, 0, ${Math.min(0.6, elevation * 0.04)})`;\n          dispCtx.fillRect(\n            x + gap / 2 + elevation * 1.5,\n            y + gap / 2 + elevation * 2.0,\n            cellSize - gap,\n            cellSize - gap,\n          );\n        }\n\n        // Draw side faces for 3D effect - thicker sides\n        if (elevation > 0.5) {\n          // Right side\n          dispCtx.fillStyle = `rgb(${Math.max(0, pixel.r - 80)}, ${Math.max(0, pixel.g - 80)}, ${Math.max(0, pixel.b - 80)})`;\n          dispCtx.beginPath();\n          dispCtx.moveTo(\n            x + cellSize - gap / 2 + offsetX,\n            y + gap / 2 + offsetY,\n          );\n          dispCtx.lineTo(x + cellSize - gap / 2, y + gap / 2);\n          dispCtx.lineTo(x + cellSize - gap / 2, y + cellSize - gap / 2);\n          dispCtx.lineTo(\n            x + cellSize - gap / 2 + offsetX,\n            y + cellSize - gap / 2 + offsetY,\n          );\n          dispCtx.closePath();\n          dispCtx.fill();\n\n          // Bottom side\n          dispCtx.fillStyle = `rgb(${Math.max(0, pixel.r - 50)}, ${Math.max(0, pixel.g - 50)}, ${Math.max(0, pixel.b - 50)})`;\n          dispCtx.beginPath();\n          dispCtx.moveTo(\n            x + gap / 2 + offsetX,\n            y + cellSize - gap / 2 + offsetY,\n          );\n          dispCtx.lineTo(x + gap / 2, y + cellSize - gap / 2);\n          dispCtx.lineTo(x + cellSize - gap / 2, y + cellSize - gap / 2);\n          dispCtx.lineTo(\n            x + cellSize - gap / 2 + offsetX,\n            y + cellSize - gap / 2 + offsetY,\n          );\n          dispCtx.closePath();\n          dispCtx.fill();\n        }\n\n        // Draw top face (main cell) - brighter when elevated\n        const brightness = 1 + elevation * 0.05;\n        dispCtx.fillStyle = `rgb(${Math.min(255, Math.round(pixel.r * brightness))}, ${Math.min(255, Math.round(pixel.g * brightness))}, ${Math.min(255, Math.round(pixel.b * brightness))})`;\n        dispCtx.fillRect(\n          x + gap / 2 + offsetX,\n          y + gap / 2 + offsetY,\n          cellSize - gap,\n          cellSize - gap,\n        );\n\n        // Draw light border around top face\n        dispCtx.strokeStyle = `rgba(${borderRGB.r}, ${borderRGB.g}, ${borderRGB.b}, ${borderOpacity + elevation * 0.008})`;\n        dispCtx.lineWidth = 0.5;\n        dispCtx.strokeRect(\n          x + gap / 2 + offsetX,\n          y + gap / 2 + offsetY,\n          cellSize - gap,\n          cellSize - gap,\n        );\n      }\n    }\n\n    animationRef.current = requestAnimationFrame(render);\n  }, [\n    gridCols,\n    gridRows,\n    mirror,\n    motionSensitivity,\n    colorMode,\n    monoRGB,\n    maxElevation,\n    elevationSmoothing,\n    backgroundColor,\n    gapRatio,\n    invertColors,\n    darken,\n    borderRGB,\n    borderOpacity,\n  ]);\n\n  // Start render loop when ready\n  useEffect(() => {\n    if (!isReady) return;\n\n    animationRef.current = requestAnimationFrame(render);\n\n    return () => {\n      cancelAnimationFrame(animationRef.current);\n    };\n  }, [isReady, render]);\n\n  return (\n    <div className={cn(\"relative h-full w-full\", className)}>\n      {/* Hidden video element */}\n      <video\n        ref={videoRef}\n        className=\"pointer-events-none absolute h-0 w-0 opacity-0\"\n        playsInline\n        muted\n      />\n\n      {/* Hidden processing canvas */}\n      <canvas\n        ref={processingCanvasRef}\n        className=\"pointer-events-none absolute h-0 w-0 opacity-0\"\n      />\n\n      {/* Display canvas with fade-in */}\n      <canvas\n        ref={displayCanvasRef}\n        className={cn(\n          \"h-full w-full transition-opacity duration-1000\",\n          isReady ? \"opacity-100\" : \"opacity-0\",\n        )}\n        style={{ backgroundColor }}\n      />\n\n      {/* Error popup */}\n      {error && showErrorPopup && (\n        <div className=\"animate-in fade-in slide-in-from-top-2 fixed top-4 right-4 z-50 duration-300\">\n          <div className=\"relative flex max-w-sm items-start gap-3 rounded-lg border border-white/10 bg-black/80 p-4 shadow-2xl backdrop-blur-xl\">\n            {/* Close button */}\n            <button\n              onClick={() => setShowErrorPopup(false)}\n              className=\"absolute top-2 right-2 rounded-md p-1 text-white/40 transition-colors hover:bg-white/10 hover:text-white/70\"\n            >\n              <svg\n                className=\"h-4 w-4\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n              >\n                <path\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={2}\n                  d=\"M6 18L18 6M6 6l12 12\"\n                />\n              </svg>\n            </button>\n\n            {/* Camera icon */}\n            <div className=\"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-white/10\">\n              <svg\n                className=\"h-5 w-5 text-white/60\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n              >\n                <path\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  strokeWidth={1.5}\n                  d=\"M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z\"\n                />\n              </svg>\n            </div>\n\n            {/* Content */}\n            <div className=\"flex-1 pr-4\">\n              <p className=\"text-sm font-medium text-white/90\">\n                Camera access needed\n              </p>\n              <p className=\"mt-1 text-xs text-white/50\">\n                Enable camera for the interactive background effect\n              </p>\n              <button\n                onClick={requestCameraAccess}\n                className=\"mt-3 inline-flex items-center gap-1.5 rounded-md bg-white/10 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-white/20\"\n              >\n                <svg\n                  className=\"h-3.5 w-3.5\"\n                  fill=\"none\"\n                  viewBox=\"0 0 24 24\"\n                  stroke=\"currentColor\"\n                >\n                  <path\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                    strokeWidth={2}\n                    d=\"M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z\"\n                  />\n                </svg>\n                Enable Camera\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Minimized error indicator */}\n      {error && !showErrorPopup && (\n        <button\n          onClick={() => setShowErrorPopup(true)}\n          className=\"fixed top-4 right-4 z-50 flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-black/60 text-white/50 shadow-lg backdrop-blur-xl transition-all hover:scale-105 hover:bg-black/80 hover:text-white/80\"\n          title=\"Camera access required\"\n        >\n          <svg\n            className=\"h-5 w-5\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n            stroke=\"currentColor\"\n          >\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={1.5}\n              d=\"M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z\"\n            />\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={2}\n              d=\"M3 3l18 18\"\n              className=\"text-red-400\"\n              stroke=\"currentColor\"\n            />\n          </svg>\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport default WebcamPixelGrid;\n",
      "type": "registry:ui",
      "target": "components/ui/webcam-pixel-grid.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Webcam Pixel Grid"
}