{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-mask-grid",
  "type": "registry:ui",
  "title": "Text Mask Grid",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Large kinetic typographic heading masked to a retro flickering canvas grid. The animated grid is only visible within the letterforms, powered by inline SVG viewBox mask data URIs, hardware GPU composite blending, and Framer Motion entrance physics.",
  "dependencies": [
    "framer-motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/text-mask-grid.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { motion, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ */\n/* Types & Exported Interface                                         */\n/* ------------------------------------------------------------------ */\n\nexport interface TextMaskGridProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"color\"> {\n  /** Text revealed through the flickering grid. Default: \"Brand\" */\n  text?: string;\n  /** Responsive CSS font-size clamp or size string. Default: \"clamp(4rem, 18vw, 16rem)\" */\n  fontSize?: string;\n  /** Font weight of the letterforms. Default: 800 */\n  fontWeight?: number | string;\n  /** Font family for the letterforms. Default: \"system-ui, -apple-system, sans-serif\" */\n  fontFamily?: string;\n  /** Probability of each square flickering per second [0..1]. Default: 0.35 */\n  flickerChance?: number;\n  /** Size of each square grid cell in pixels. Default: 4 */\n  squareSize?: number;\n  /** Gap between adjacent grid squares in pixels. Default: 5 */\n  gridGap?: number;\n  /** Maximum opacity ceiling for the grid squares [0..1]. Default: 0.4 */\n  maxOpacity?: number;\n  /** Color of the grid squares. Default: \"currentColor\" */\n  color?: string;\n  /** ViewBox width for the SVG mask coordinate space. Default: 1000 */\n  viewBoxWidth?: number;\n  /** ViewBox height for the SVG mask coordinate space. Default: 260 */\n  viewBoxHeight?: number;\n  /** Whether to animate letters with a staggered entrance. Default: false */\n  stagger?: boolean;\n  /** Additional Tailwind CSS classes merged into container */\n  className?: string;\n}\n\n/* ------------------------------------------------------------------ */\n/* Self-Contained Color Parser & Canvas Grid Engine                   */\n/* ------------------------------------------------------------------ */\n\nfunction parseColorToRgbaPrefix(colorStr: string): string {\n  if (typeof window === \"undefined\") {\n    return \"rgba(0, 0, 0, \";\n  }\n\n  // Fast-path for hex (#fff, #ffffff, #ffffff80)\n  if (colorStr.startsWith(\"#\")) {\n    let hex = colorStr.slice(1);\n    if (hex.length === 3) {\n      hex = hex\n        .split(\"\")\n        .map((c) => c + c)\n        .join(\"\");\n    }\n    const num = parseInt(hex.slice(0, 6), 16);\n    if (!Number.isNaN(num)) {\n      const r = (num >> 16) & 255;\n      const g = (num >> 8) & 255;\n      const b = num & 255;\n      return `rgba(${r}, ${g}, ${b}, `;\n    }\n  }\n\n  // Fast-path for rgb(r, g, b) or rgba(r, g, b, a)\n  const rgbMatch = colorStr.match(/rgba?\\((\\d+)[,\\s]+(\\d+)[,\\s]+(\\d+)/i);\n  if (rgbMatch) {\n    return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, `;\n  }\n\n  // DOM 1x1 canvas fallback for named colors (e.g. \"currentColor\", \"tomato\")\n  try {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = canvas.height = 1;\n    const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n    if (!ctx) return \"rgba(0, 0, 0, \";\n    ctx.fillStyle = colorStr;\n    ctx.fillRect(0, 0, 1, 1);\n    const data = ctx.getImageData(0, 0, 1, 1).data;\n    return `rgba(${data[0]}, ${data[1]}, ${data[2]}, `;\n  } catch {\n    return \"rgba(0, 0, 0, \";\n  }\n}\n\ninterface InternalGridCanvasProps {\n  squareSize: number;\n  gridGap: number;\n  flickerChance: number;\n  maxOpacity: number;\n  color: string;\n  className?: string;\n}\n\n/**\n * Self-contained canvas engine embedded directly within TextMaskGrid\n * so TextMaskGrid has 0 external file dependencies.\n */\nfunction InternalGridCanvas({\n  squareSize,\n  gridGap,\n  flickerChance,\n  maxOpacity,\n  color,\n  className,\n}: InternalGridCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isInView, setIsInView] = useState(false);\n  const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });\n\n  const memoizedColorPrefix = useMemo(() => {\n    return parseColorToRgbaPrefix(color);\n  }, [color]);\n\n  const setupCanvas = useCallback(\n    (canvas: HTMLCanvasElement, w: number, h: number) => {\n      if (w <= 0 || h <= 0) return null;\n      const dpr = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n      canvas.width = Math.floor(w * dpr);\n      canvas.height = Math.floor(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n\n      const cellSize = squareSize + gridGap;\n      const cols = Math.ceil(w / cellSize);\n      const rows = Math.ceil(h / cellSize);\n\n      const squares = new Float32Array(cols * rows);\n      for (let i = 0; i < squares.length; i++) {\n        squares[i] = Math.random() * maxOpacity;\n      }\n\n      return { cols, rows, squares, dpr };\n    },\n    [squareSize, gridGap, maxOpacity]\n  );\n\n  const updateSquares = useCallback(\n    (squares: Float32Array, deltaTime: number) => {\n      const stepChance = flickerChance * deltaTime;\n      for (let i = 0; i < squares.length; i++) {\n        if (Math.random() < stepChance) {\n          squares[i] = Math.random() * maxOpacity;\n        }\n      }\n    },\n    [flickerChance, maxOpacity]\n  );\n\n  const drawGrid = useCallback(\n    (\n      ctx: CanvasRenderingContext2D,\n      w: number,\n      h: number,\n      cols: number,\n      rows: number,\n      squares: Float32Array,\n      dpr: number\n    ) => {\n      ctx.clearRect(0, 0, w, h);\n\n      const cellSize = (squareSize + gridGap) * dpr;\n      const size = squareSize * dpr;\n\n      for (let i = 0; i < cols; i++) {\n        const x = Math.floor(i * cellSize);\n        for (let j = 0; j < rows; j++) {\n          const idx = i * rows + j;\n          if (idx >= squares.length) continue;\n          const opacity = squares[idx];\n          ctx.fillStyle = `${memoizedColorPrefix}${opacity})`;\n          const y = Math.floor(j * cellSize);\n          ctx.fillRect(x, y, size, size);\n        }\n      }\n    },\n    [memoizedColorPrefix, squareSize, gridGap]\n  );\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext(\"2d\", { alpha: true });\n    if (!ctx) return;\n\n    let animationFrameId: number | null = null;\n    let resizeObserver: ResizeObserver | null = null;\n    let intersectionObserver: IntersectionObserver | null = null;\n    let gridParams: ReturnType<typeof setupCanvas> = null;\n\n    const updateCanvasSize = () => {\n      const newWidth = container.clientWidth;\n      const newHeight = container.clientHeight;\n      if (newWidth <= 0 || newHeight <= 0) return;\n      setCanvasSize({ width: newWidth, height: newHeight });\n      gridParams = setupCanvas(canvas, newWidth, newHeight);\n    };\n\n    updateCanvasSize();\n\n    let lastTime = performance.now();\n    const animate = (time: number) => {\n      if (!isInView || !gridParams) return;\n\n      const deltaTime = Math.min((time - lastTime) / 1000, 0.1);\n      lastTime = time;\n\n      updateSquares(gridParams.squares, deltaTime);\n      drawGrid(\n        ctx,\n        canvas.width,\n        canvas.height,\n        gridParams.cols,\n        gridParams.rows,\n        gridParams.squares,\n        gridParams.dpr\n      );\n      animationFrameId = requestAnimationFrame(animate);\n    };\n\n    resizeObserver = new ResizeObserver(() => {\n      updateCanvasSize();\n    });\n    resizeObserver.observe(container);\n\n    intersectionObserver = new IntersectionObserver(\n      ([entry]) => {\n        setIsInView(entry.isIntersecting);\n      },\n      { threshold: 0 }\n    );\n    intersectionObserver.observe(container);\n\n    if (isInView && gridParams) {\n      animationFrameId = requestAnimationFrame(animate);\n    }\n\n    return () => {\n      if (animationFrameId !== null) {\n        cancelAnimationFrame(animationFrameId);\n      }\n      if (resizeObserver) {\n        resizeObserver.disconnect();\n      }\n      if (intersectionObserver) {\n        intersectionObserver.disconnect();\n      }\n    };\n  }, [setupCanvas, updateSquares, drawGrid, isInView]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"h-full w-full overflow-hidden\", className)}\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"pointer-events-none block\"\n        style={{\n          width: canvasSize.width,\n          height: canvasSize.height,\n        }}\n      />\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Helper Functions                                                   */\n/* ------------------------------------------------------------------ */\n\nfunction escapeSvgText(str: string): string {\n  return str\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\")\n    .replace(/'/g, \"&apos;\");\n}\n\n/* ------------------------------------------------------------------ */\n/* Main TextMaskGrid Component (100% Self-Contained)                  */\n/* ------------------------------------------------------------------ */\n\nexport const TextMaskGrid: React.FC<TextMaskGridProps> = ({\n  text = \"Brand\",\n  fontSize = \"clamp(4rem, 18vw, 16rem)\",\n  fontWeight = 800,\n  fontFamily = \"Inter Tight, system-ui, -apple-system, sans-serif\",\n  flickerChance = 0.35,\n  squareSize = 4,\n  gridGap = 5,\n  maxOpacity = 0.4,\n  color = \"currentColor\",\n  viewBoxWidth = 1000,\n  viewBoxHeight = 260,\n  stagger = false,\n  className,\n  ...props\n}) => {\n  const [supportsMask, setSupportsMask] = useState(true);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Verify browser mask-image support\n  useEffect(() => {\n    if (typeof window !== \"undefined\" && typeof CSS !== \"undefined\") {\n      const isSupported =\n        CSS.supports(\"mask-image\", \"url(data:image/svg+xml;utf8,<svg></svg>)\") ||\n        CSS.supports(\"-webkit-mask-image\", \"url(data:image/svg+xml;utf8,<svg></svg>)\") ||\n        CSS.supports(\"mask-image\", \"none\") ||\n        CSS.supports(\"-webkit-mask-image\", \"none\");\n      setSupportsMask(isSupported);\n    }\n  }, []);\n\n  /**\n   * Generates an inline SVG <text> mask data URI.\n   *\n   * WHY THIS APPROACH:\n   * 1. Vector Scalability: Scalable viewBox maintains crisp subpixel edges across any DPR without raster pixelation.\n   * 2. Zero Network Overhead: Self-contained data URI requires no external image requests or font subsetting.\n   * 3. Hardware-Accelerated: The GPU composite engine masks the HTML5 canvas layer in a single blend pass.\n   * 4. Auto-Centering: Using text-anchor=\"middle\" and dominant-baseline=\"central\" ensures optical alignment.\n   */\n  const maskDataUri = useMemo(() => {\n    const escaped = escapeSvgText(text);\n\n    // Compute an optimal font-size in viewBox units so the text fits within the coordinate space\n    const charCount = Math.max(text.length, 1);\n    const approxCharWidthRatio = 0.62;\n    const maxAvailableWidth = viewBoxWidth * 0.94;\n    const computedSvgFontSize = Math.min(\n      viewBoxHeight * 0.78,\n      maxAvailableWidth / (charCount * approxCharWidthRatio)\n    );\n\n    const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${viewBoxWidth} ${viewBoxHeight}' width='100%' height='100%' preserveAspectRatio='xMidYMid meet'><text x='50%' y='52%' text-anchor='middle' dominant-baseline='central' font-size='${computedSvgFontSize}' font-weight='${fontWeight}' font-family='${fontFamily}' fill='white'>${escaped}</text></svg>`;\n\n    return `url(\"data:image/svg+xml;utf8,${encodeURIComponent(svg)}\")`;\n  }, [text, fontWeight, fontFamily, viewBoxWidth, viewBoxHeight]);\n\n  const maskStyles: React.CSSProperties = {\n    WebkitMaskImage: maskDataUri,\n    maskImage: maskDataUri,\n    WebkitMaskRepeat: \"no-repeat\",\n    maskRepeat: \"no-repeat\",\n    WebkitMaskPosition: \"center\",\n    maskPosition: \"center\",\n    WebkitMaskSize: \"contain\",\n    maskSize: \"contain\",\n  };\n\n  // Staggered letter container variants\n  const containerVariants = {\n    hidden: { opacity: 0 },\n    visible: {\n      opacity: 1,\n      transition: {\n        staggerChildren: 0.08,\n      },\n    },\n  };\n\n  const letterVariants = {\n    hidden: {\n      opacity: 0,\n      y: 24,\n      filter: \"blur(8px)\",\n      scale: 0.92,\n    },\n    visible: {\n      opacity: 1,\n      y: 0,\n      filter: \"blur(0px)\",\n      scale: 1,\n      transition: {\n        duration: 0.75,\n        ease: [0.16, 1, 0.3, 1],\n      },\n    },\n  };\n\n  return (\n    <motion.div\n      role=\"img\"\n      aria-label={text}\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1, scale: 1 }\n          : { opacity: 0, scale: 0.96, filter: \"blur(12px)\" }\n      }\n      whileInView={{\n        opacity: 1,\n        scale: 1,\n        filter: \"blur(0px)\",\n      }}\n      whileHover={\n        shouldReduceMotion\n          ? undefined\n          : {\n              scale: 1.02,\n              transition: { type: \"spring\", stiffness: 300, damping: 20 },\n            }\n      }\n      viewport={{ once: true, margin: \"-40px\" }}\n      transition={{\n        duration: 0.9,\n        ease: [0.16, 1, 0.3, 1],\n      }}\n      className={cn(\n        \"relative flex w-full items-center justify-center overflow-hidden select-none\",\n        className\n      )}\n      {...(props as any)}\n    >\n      {/* ── Standard Single SVG Mask Mode (Recommended: 60-120 FPS) ── */}\n      {!stagger && (\n        <div\n          style={supportsMask ? maskStyles : undefined}\n          className=\"relative flex h-full w-full items-center justify-center\"\n        >\n          {supportsMask ? (\n            <div className=\"relative h-full w-full min-h-[140px] sm:min-h-[200px] md:min-h-[260px]\">\n              <InternalGridCanvas\n                squareSize={squareSize}\n                gridGap={gridGap}\n                flickerChance={flickerChance}\n                maxOpacity={maxOpacity}\n                color={color}\n                className=\"absolute inset-0 h-full w-full\"\n              />\n            </div>\n          ) : (\n            /* Fallback when mask-image is unsupported */\n            <h1\n              style={{ fontSize, fontWeight }}\n              className=\"bg-gradient-to-b from-neutral-900 to-neutral-700 bg-clip-text text-center text-transparent dark:from-white dark:to-neutral-400\"\n            >\n              {text}\n            </h1>\n          )}\n        </div>\n      )}\n\n      {/* ── Optional Staggered Letter Entrance Mode ── */}\n      {stagger && (\n        <motion.div\n          variants={containerVariants}\n          initial=\"hidden\"\n          whileInView=\"visible\"\n          viewport={{ once: true }}\n          className=\"flex flex-wrap items-center justify-center\"\n        >\n          {text.split(\"\").map((char, idx) => {\n            if (char === \" \") {\n              return <span key={idx} className=\"w-4\" />;\n            }\n\n            const charSvg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120' width='100%' height='100%' preserveAspectRatio='xMidYMid meet'><text x='50%' y='52%' text-anchor='middle' dominant-baseline='central' font-size='92' font-weight='${fontWeight}' font-family='${fontFamily}' fill='white'>${escapeSvgText(char)}</text></svg>`;\n            const charMaskUri = `url(\"data:image/svg+xml;utf8,${encodeURIComponent(charSvg)}\")`;\n\n            return (\n              <motion.div\n                key={idx}\n                variants={letterVariants}\n                className=\"relative inline-block overflow-hidden\"\n                style={{\n                  width: \"clamp(2.5rem, 8vw, 7rem)\",\n                  height: \"clamp(3.5rem, 11vw, 9.5rem)\",\n                  WebkitMaskImage: charMaskUri,\n                  maskImage: charMaskUri,\n                  WebkitMaskRepeat: \"no-repeat\",\n                  maskRepeat: \"no-repeat\",\n                  WebkitMaskPosition: \"center\",\n                  maskPosition: \"center\",\n                  WebkitMaskSize: \"contain\",\n                  maskSize: \"contain\",\n                }}\n              >\n                <InternalGridCanvas\n                  squareSize={squareSize}\n                  gridGap={gridGap}\n                  flickerChance={flickerChance}\n                  maxOpacity={maxOpacity}\n                  color={color}\n                  className=\"absolute inset-0 h-full w-full\"\n                />\n              </motion.div>\n            );\n          })}\n        </motion.div>\n      )}\n    </motion.div>\n  );\n};\n\nexport default TextMaskGrid;\n",
      "type": "registry:ui",
      "target": "components/ui/text-mask-grid.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
