{
  "name": "ascii-art",
  "type": "registry:ui",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/ascii-art.tsx",
      "content": "\"use client\";\nimport React, {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  useCallback,\n  useId,\n} from \"react\";\nimport { motion, useInView } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst useIsomorphicLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nconst ASCII_CHARSETS = {\n  standard: \" .,:;i1tfLCG08@\",\n  blocks: \" ░▒▓█\",\n  binary: \" 01\",\n  dots: \" ·•●\",\n  minimal: \" .:░▒\",\n  dense: \" .'`^\\\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$\",\n  arrows: \" ←↑→↓↔↕↖↗↘↙\",\n  stars: \" ·✦✧★\",\n  hash: \" -=#\",\n  pipes: \" |/─\\\\│\",\n  braille: \" ⠁⠃⠇⠏⠟⠿⡿⣿\",\n  circles: \" ○◔◑◕●\",\n  squares: \" ▢▣▤▥▦▧▨▩\",\n  hearts: \" ♡♥\",\n  math: \" +-×÷=≠≈∞\",\n} as const;\n\ntype CharsetPreset = keyof typeof ASCII_CHARSETS;\n\nconst isCharsetPreset = (value: string): value is CharsetPreset => {\n  return value in ASCII_CHARSETS;\n};\n\nconst resolveCharset = (charset: string): string => {\n  if (isCharsetPreset(charset)) {\n    return ASCII_CHARSETS[charset];\n  }\n  return charset;\n};\n\nconst resolveCssColor = (\n  color: string,\n  element: HTMLElement | null\n): string => {\n  if (!color) return color;\n\n  if (color.startsWith(\"var(\")) {\n    if (!element) return \"#ffffff\";\n\n    const tempDiv = document.createElement(\"div\");\n    tempDiv.style.color = color;\n    element.appendChild(tempDiv);\n    const computedColor = getComputedStyle(tempDiv).color;\n    element.removeChild(tempDiv);\n    return computedColor || \"#ffffff\";\n  }\n\n  return color;\n};\n\ntype AsciiArtProps = {\n  src: string;\n  /** Number of ASCII columns (character resolution). Higher = more detail. */\n  resolution?: number;\n  /** Charset preset name (\"standard\", \"blocks\", \"binary\", etc.) or custom character string */\n  charset?: CharsetPreset | string;\n  /** Text color for the ASCII art (ignored if colored=true) */\n  color?: string;\n  /** Background color */\n  backgroundColor?: string;\n  /** Convert to inverted colors (dark bg, light text) */\n  inverted?: boolean;\n  /** Enable colored ASCII (uses image colors) */\n  colored?: boolean;\n  /** Enable animation on load */\n  animated?: boolean;\n  /** Animation style */\n  animationStyle?: \"fade\" | \"typewriter\" | \"matrix\" | \"none\";\n  /** Duration for fade animation in seconds */\n  animationDuration?: number;\n  /** Font family for ASCII characters */\n  fontFamily?: string;\n  /** Container className - use this to control size (e.g., w-full, h-64) */\n  className?: string;\n  /** Only animate when in view */\n  animateOnView?: boolean;\n  /** How the image should fit within the ASCII grid */\n  objectFit?: \"cover\" | \"contain\" | \"fill\";\n};\nconst MATRIX_CHARSET = \"ﾊﾐﾋｰｳｼﾅﾓﾆｻﾜﾂｵﾘｱﾎﾃﾏｹﾒｴｶｷﾑﾕﾗｾﾈｽﾀﾇﾍ\";\n\ntype AsciiPixel = {\n  char: string;\n  r: number;\n  g: number;\n  b: number;\n};\n\nexport const AsciiArt: React.FC<AsciiArtProps> = ({\n  src,\n  resolution = 80,\n  charset = \"standard\",\n  color = \"#ffffff\",\n  backgroundColor = \"transparent\",\n  inverted = false,\n  colored = false,\n  animated = true,\n  animationStyle = \"fade\",\n  animationDuration = 1,\n  fontFamily = \"monospace\",\n  className,\n  animateOnView = true,\n  objectFit = \"cover\",\n}) => {\n  const uniqueId = useId();\n  const [asciiData, setAsciiData] = useState<AsciiPixel[][]>([]);\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [hasAnimated, setHasAnimated] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const animationRef = useRef<number | null>(null);\n  const isInView = useInView(containerRef, { once: true, amount: 0.1 });\n\n  const shouldStartAnimation = animated && animateOnView ? isInView : animated;\n  const shouldShowStatic = !animated || animationStyle === \"none\";\n\n  const resolvedCharset = resolveCharset(charset);\n  const effectiveCharset = inverted\n    ? resolvedCharset.split(\"\").reverse().join(\"\")\n    : resolvedCharset;\n\n  const defaultColor = inverted ? \"#ffffff\" : \"#000000\";\n  const textColor = color || defaultColor;\n\n  useEffect(() => {\n    let isCancelled = false;\n\n    const img = new Image();\n    img.crossOrigin = \"anonymous\";\n    img.src = src;\n\n    img.onload = () => {\n      if (isCancelled) return;\n\n      const canvas = document.createElement(\"canvas\");\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) {\n        setError(\"Canvas context not available\");\n        return;\n      }\n\n      const imgWidth = img.naturalWidth;\n      const imgHeight = img.naturalHeight;\n      const imgAspect = imgWidth / imgHeight;\n      const charAspectRatio = 0.55;\n\n      const cols = resolution;\n      const rows = Math.floor(cols * charAspectRatio);\n\n      canvas.width = cols;\n      canvas.height = rows;\n\n      const visualAspect = 1.0;\n\n      let sx = 0,\n        sy = 0,\n        sw = imgWidth,\n        sh = imgHeight;\n\n      if (objectFit === \"cover\") {\n        if (imgAspect > visualAspect) {\n          sw = imgHeight * visualAspect;\n          sx = (imgWidth - sw) / 2;\n        } else {\n          sh = imgWidth / visualAspect;\n          sy = (imgHeight - sh) / 2;\n        }\n      } else if (objectFit === \"contain\") {\n        ctx.fillStyle = \"#000000\";\n        ctx.fillRect(0, 0, cols, rows);\n\n        let dw, dh, dx, dy;\n        if (imgAspect > visualAspect) {\n          dw = cols;\n          dh = cols / imgAspect * charAspectRatio;\n          dx = 0;\n          dy = (rows - dh) / 2;\n        } else {\n          dh = rows;\n          dw = rows * imgAspect / charAspectRatio;\n          dx = (cols - dw) / 2;\n          dy = 0;\n        }\n        ctx.drawImage(img, dx, dy, dw, dh);\n      }\n\n      if (objectFit !== \"contain\") {\n        ctx.drawImage(img, sx, sy, sw, sh, 0, 0, cols, rows);\n      }\n\n      let imageData: ImageData;\n      try {\n        imageData = ctx.getImageData(0, 0, cols, rows);\n      } catch {\n        setError(\"Unable to read image data (CORS issue)\");\n        return;\n      }\n\n      const data = imageData.data;\n      const result: AsciiPixel[][] = [];\n\n      for (let y = 0; y < rows; y++) {\n        const row: AsciiPixel[] = [];\n        for (let x = 0; x < cols; x++) {\n          const idx = (y * cols + x) * 4;\n          const r = data[idx];\n          const g = data[idx + 1];\n          const b = data[idx + 2];\n          const a = data[idx + 3];\n\n          const brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n          const adjustedBrightness = a === 0 ? 0 : brightness;\n\n          const charIndex = Math.floor(\n            adjustedBrightness * (effectiveCharset.length - 1)\n          );\n          const char = effectiveCharset[charIndex] || \" \";\n\n          row.push({ char, r, g, b });\n        }\n        result.push(row);\n      }\n\n      setAsciiData(result);\n      setIsLoaded(true);\n    };\n\n    img.onerror = () => {\n      if (isCancelled) return;\n      setError(\"Failed to load image\");\n    };\n\n    return () => {\n      isCancelled = true;\n    };\n  }, [src, resolution, effectiveCharset, objectFit]);\n\n  const drawCanvas = useCallback(\n    (progress: number = 1, matrixProgress?: number) => {\n      const canvas = canvasRef.current;\n      const container = containerRef.current;\n      if (!canvas || !container || asciiData.length === 0) return;\n\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) return;\n\n      const dpr = window.devicePixelRatio || 1;\n      const containerWidth = container.clientWidth;\n      const containerHeight = container.clientHeight;\n\n      if (containerWidth === 0 || containerHeight === 0) return;\n\n      canvas.width = containerWidth * dpr;\n      canvas.height = containerHeight * dpr;\n      canvas.style.width = `${containerWidth}px`;\n      canvas.style.height = `${containerHeight}px`;\n      ctx.scale(dpr, dpr);\n\n      const resolvedBgColor = resolveCssColor(backgroundColor, container);\n      const resolvedTextColor = resolveCssColor(textColor, container);\n\n      if (resolvedBgColor !== \"transparent\") {\n        ctx.fillStyle = resolvedBgColor;\n        ctx.fillRect(0, 0, containerWidth, containerHeight);\n      } else {\n        ctx.clearRect(0, 0, containerWidth, containerHeight);\n      }\n\n      const rows = asciiData.length;\n      const cols = asciiData[0]?.length || 0;\n      if (cols === 0) return;\n\n      const charWidth = containerWidth / cols;\n      const charHeight = containerHeight / rows;\n      const fontSize = Math.min(charWidth * 1.8, charHeight * 1.2);\n\n      ctx.font = `${fontSize}px ${fontFamily}`;\n      ctx.textBaseline = \"top\";\n      ctx.textAlign = \"center\";\n\n      const totalChars = rows * cols;\n      const revealedChars = Math.floor(progress * totalChars);\n\n      let charIndex = 0;\n      for (let y = 0; y < rows; y++) {\n        for (let x = 0; x < cols; x++) {\n          const pixel = asciiData[y][x];\n          const cx = x * charWidth + charWidth / 2;\n          const cy = y * charHeight;\n\n          if (animationStyle === \"typewriter\" && charIndex >= revealedChars) {\n            charIndex++;\n            continue;\n          }\n\n          let displayChar = pixel.char;\n          let displayColor = colored\n            ? `rgb(${pixel.r}, ${pixel.g}, ${pixel.b})`\n            : resolvedTextColor;\n\n          if (animationStyle === \"matrix\" && matrixProgress !== undefined) {\n            const charProgress = (x * 0.02 + y * 0.01) / 2;\n            if (matrixProgress < charProgress) {\n              charIndex++;\n              continue;\n            } else if (matrixProgress < charProgress + 0.15) {\n              displayChar =\n                MATRIX_CHARSET[\n                  Math.floor(Math.random() * MATRIX_CHARSET.length)\n                ];\n              displayColor = \"#00ff00\";\n              ctx.shadowColor = \"#00ff00\";\n              ctx.shadowBlur = 5;\n            } else {\n              ctx.shadowBlur = 0;\n            }\n          }\n\n          ctx.fillStyle = displayColor;\n          ctx.globalAlpha = animationStyle === \"fade\" ? progress : 1;\n          ctx.fillText(displayChar, cx, cy);\n\n          charIndex++;\n        }\n      }\n\n      ctx.globalAlpha = 1;\n      ctx.shadowBlur = 0;\n    },\n    [\n      asciiData,\n      backgroundColor,\n      colored,\n      textColor,\n      fontFamily,\n      animationStyle,\n    ]\n  );\n\n  useEffect(() => {\n    if (!isLoaded || asciiData.length === 0) return;\n\n    const draw = () => {\n      const canvas = canvasRef.current;\n      const container = containerRef.current;\n      if (!canvas || !container) {\n        requestAnimationFrame(draw);\n        return;\n      }\n\n      if (shouldShowStatic || hasAnimated || !shouldStartAnimation) {\n        drawCanvas(1);\n        return;\n      }\n\n      const startTime = performance.now();\n      const duration =\n        animationStyle === \"fade\"\n          ? animationDuration * 1000\n          : animationStyle === \"typewriter\"\n            ? asciiData.length * asciiData[0]?.length * 2\n            : animationStyle === \"matrix\"\n              ? 3000\n              : 1000;\n\n      const animate = (currentTime: number) => {\n        const elapsed = currentTime - startTime;\n        const progress = Math.min(elapsed / duration, 1);\n\n        if (animationStyle === \"matrix\") {\n          drawCanvas(1, progress);\n        } else {\n          drawCanvas(progress);\n        }\n\n        if (progress < 1) {\n          animationRef.current = requestAnimationFrame(animate);\n        } else {\n          setHasAnimated(true);\n        }\n      };\n\n      animationRef.current = requestAnimationFrame(animate);\n    };\n\n    const frameId = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(frameId);\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current);\n      }\n    };\n  }, [\n    isLoaded,\n    shouldStartAnimation,\n    shouldShowStatic,\n    hasAnimated,\n    animationStyle,\n    animationDuration,\n    drawCanvas,\n    asciiData,\n  ]);\n\n  useIsomorphicLayoutEffect(() => {\n    if (!isLoaded || asciiData.length === 0) return;\n\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) return;\n\n    drawCanvas(1);\n  }, [isLoaded, asciiData, drawCanvas]);\n\n  useEffect(() => {\n    if (!isLoaded || asciiData.length === 0) return;\n\n    const container = containerRef.current;\n    if (!container) return;\n\n    const resizeObserver = new ResizeObserver(() => {\n      drawCanvas(1);\n    });\n\n    resizeObserver.observe(container);\n\n    return () => resizeObserver.disconnect();\n  }, [isLoaded, asciiData, drawCanvas]);\n\n  if (error) {\n    return (\n      <div\n        className={cn(\n          \"flex items-center justify-center text-red-500 text-sm font-mono\",\n          className\n        )}\n      >\n        Error: {error}\n      </div>\n    );\n  }\n\n  if (!isLoaded) {\n    return (\n      <div\n        className={cn(\n          \"flex items-center justify-center text-neutral-500 text-sm font-mono animate-pulse\",\n          className\n        )}\n        style={{ backgroundColor }}\n      >\n        Loading...\n      </div>\n    );\n  }\n\n  const canvasElement = (\n    <canvas\n      key={uniqueId}\n      id={`ascii-canvas-${uniqueId}`}\n      ref={canvasRef}\n      className=\"block w-full h-full\"\n      aria-label=\"ASCII art rendering of image\"\n      role=\"img\"\n    />\n  );\n\n  if (animationStyle === \"fade\" && animated && !hasAnimated) {\n    return (\n      <motion.div\n        ref={containerRef}\n        className={cn(\"overflow-hidden\", className)}\n        style={{ backgroundColor }}\n        initial={{ opacity: 0 }}\n        animate={shouldStartAnimation ? { opacity: 1 } : { opacity: 0 }}\n        transition={{ duration: animationDuration * 0.3 }}\n      >\n        {canvasElement}\n      </motion.div>\n    );\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"overflow-hidden\", className)}\n      style={{ backgroundColor }}\n    >\n      {canvasElement}\n    </div>\n  );\n};\n\nexport const AsciiArtStatic: React.FC<\n  Omit<AsciiArtProps, \"animated\" | \"animationStyle\">\n> = (props) => {\n  return <AsciiArt {...props} animated={false} animationStyle=\"none\" />;\n};\n",
      "type": "registry:ui",
      "target": "components/ui/ascii-art.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Ascii Art"
}