{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ascii-effect",
  "type": "registry:ui",
  "title": "ASCII Effect",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Render images as responsive ASCII artwork with image, flow, and glitch variations.",
  "files": [
    {
      "path": "components/ui/ascii-effect.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useRef, type HTMLAttributes, type PointerEvent } from \"react\"\n\nexport interface AsciiEffectProps extends Omit<HTMLAttributes<HTMLDivElement>, \"color\"> {\n  imageSrc: string\n  alt?: string\n  variant?: \"image\" | \"flow\" | \"glitch\"\n  chars?: string\n  fontSize?: number\n  fontFamily?: string\n  fontWeight?: number | string\n  lineHeight?: number\n  characterSpacing?: number\n  brightnessBoost?: number\n  contrast?: number\n  threshold?: number\n  posterize?: number\n  dither?: \"none\" | \"floyd-steinberg\" | \"bayer\"\n  ditherStrength?: number\n  flowSpeed?: number\n  flowDirection?: number\n  flowStrength?: number\n  flowFrequency?: number\n  mouseRadius?: number\n  mouseStrength?: number\n  mouseWaveSpeed?: number\n  scale?: number\n  fit?: \"cover\" | \"contain\" | \"stretch\"\n  colors?: string[]\n  colorMode?: \"gradient\" | \"source\"\n  backgroundColor?: string\n  invert?: boolean\n  glitchIntensity?: number\n  glitchFrequency?: number\n  revealDuration?: number\n}\n\nexport type AsciiPresetProps = Omit<AsciiEffectProps, \"variant\">\n\nconst IMAGE_COLORS = [\"#f4f4f5\", \"#a1a1aa\"]\nconst FLOW_COLORS = [\"#e2e8f0\", \"#67e8f9\", \"#818cf8\"]\nconst GLITCH_COLORS = [\"#ecfccb\", \"#a3e635\", \"#22d3ee\"]\n\nfunction clamp(value: number, min = 0, max = 1) {\n  return Math.min(max, Math.max(min, value))\n}\n\nfunction parseHex(color: string) {\n  const match = /^#([\\da-f]{2})([\\da-f]{2})([\\da-f]{2})$/i.exec(color)\n  return match\n    ? [Number.parseInt(match[1]!, 16), Number.parseInt(match[2]!, 16), Number.parseInt(match[3]!, 16)]\n    : null\n}\n\nfunction gradientColor(colors: string[], amount: number) {\n  if (colors.length < 2) return colors[0] ?? \"#ffffff\"\n\n  const position = clamp(amount) * (colors.length - 1)\n  const index = Math.min(Math.floor(position), colors.length - 2)\n  const mix = position - index\n  const from = parseHex(colors[index]!)\n  const to = parseHex(colors[index + 1]!)\n  if (!from || !to) return colors[Math.round(position)] ?? colors[0]!\n\n  return `rgb(${from.map((channel, i) => Math.round(channel + (to[i]! - channel) * mix)).join(\", \")})`\n}\n\nexport function AsciiEffect({\n  imageSrc,\n  alt = \"ASCII rendering\",\n  variant = \"image\",\n  chars = \" .:-=+*#%@\",\n  fontSize = 9,\n  fontFamily = \"Arial, Helvetica, sans-serif\",\n  fontWeight = 400,\n  lineHeight = 1,\n  characterSpacing = 1,\n  brightnessBoost = 2.2,\n  contrast = 1.1,\n  threshold = 0.06,\n  posterize = 32,\n  dither = \"floyd-steinberg\",\n  ditherStrength = 0.8,\n  flowSpeed = 0.22,\n  flowDirection = 0,\n  flowStrength = 12,\n  flowFrequency = 0.018,\n  mouseRadius = 150,\n  mouseStrength = 22,\n  mouseWaveSpeed = 1.2,\n  scale = 1.15,\n  fit = \"cover\",\n  colors = IMAGE_COLORS,\n  colorMode = \"gradient\",\n  backgroundColor = \"#07090d\",\n  invert = false,\n  glitchIntensity = 0.65,\n  glitchFrequency = 1.4,\n  revealDuration = 1400,\n  className,\n  onPointerMove,\n  onPointerLeave,\n  ...props\n}: AsciiEffectProps) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const pointer = useRef({ x: 0, y: 0, targetX: 0, targetY: 0, active: false })\n\n  useEffect(() => {\n    const container = containerRef.current\n    const canvas = canvasRef.current\n    if (!container || !canvas || chars.length === 0) return\n\n    const context = canvas.getContext(\"2d\")\n    const sampleCanvas = document.createElement(\"canvas\")\n    const sampleContext = sampleCanvas.getContext(\"2d\", { willReadFrequently: true })\n    if (!context || !sampleContext) return\n\n    const image = new Image()\n    image.crossOrigin = \"anonymous\"\n    let frame = 0\n    let width = 0\n    let height = 0\n    let startedAt = 0\n    let nextGlitchAt = 0\n    let glitchUntil = 0\n    let glitchBands = new Map<number, number>()\n    let loaded = false\n    const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect()\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      width = Math.max(1, rect.width)\n      height = Math.max(1, rect.height)\n      canvas.width = Math.round(width * dpr)\n      canvas.height = Math.round(height * dpr)\n      canvas.style.width = `${width}px`\n      canvas.style.height = `${height}px`\n      context.setTransform(dpr, 0, 0, dpr, 0, 0)\n      if (loaded) draw(performance.now())\n    }\n\n    const updateGlitch = (now: number, rows: number) => {\n      if (variant !== \"glitch\" || reduceMotion || glitchFrequency <= 0 || now < nextGlitchAt) return\n\n      glitchBands = new Map()\n      const bandCount = Math.max(1, Math.round(clamp(glitchIntensity) * 4))\n      for (let band = 0; band < bandCount; band++) {\n        const start = Math.floor(Math.random() * rows)\n        const size = 1 + Math.floor(Math.random() * 3)\n        const offset = (Math.random() - 0.5) * fontSize * 12 * clamp(glitchIntensity)\n        for (let row = start; row < Math.min(rows, start + size); row++) glitchBands.set(row, offset)\n      }\n      glitchUntil = now + 70 + 90 * clamp(glitchIntensity)\n      nextGlitchAt = now + 1000 / glitchFrequency\n    }\n\n    const draw = (now: number) => {\n      if (!loaded || width === 0 || height === 0) return\n\n      pointer.current.x += (pointer.current.targetX - pointer.current.x) * 0.08\n      pointer.current.y += (pointer.current.targetY - pointer.current.y) * 0.08\n\n      const cellHeight = Math.max(4, fontSize * Math.max(0.5, lineHeight))\n      context.font = `${fontWeight} ${fontSize}px ${fontFamily}`\n      const cellWidth = Math.max(2, context.measureText(\"M\").width * Math.max(0.5, characterSpacing))\n      const columns = Math.ceil(width / cellWidth) + 2\n      const rows = Math.ceil(height / cellHeight) + 2\n      const radians = flowDirection * Math.PI / 180\n      const directionX = Math.cos(radians)\n      const directionY = Math.sin(radians)\n      sampleCanvas.width = columns\n      sampleCanvas.height = rows\n\n      const imageScale = fit === \"stretch\"\n        ? 1\n        : (fit === \"contain\"\n            ? Math.min(width / image.naturalWidth, height / image.naturalHeight)\n            : Math.max(width / image.naturalWidth, height / image.naturalHeight)) * Math.max(0.1, scale)\n      const drawWidth = fit === \"stretch\" ? columns : image.naturalWidth * imageScale / cellWidth\n      const drawHeight = fit === \"stretch\" ? rows : image.naturalHeight * imageScale / cellHeight\n      sampleContext.clearRect(0, 0, columns, rows)\n      sampleContext.drawImage(\n        image,\n        (columns - drawWidth) / 2,\n        (rows - drawHeight) / 2,\n        drawWidth,\n        drawHeight,\n      )\n\n      const pixels = sampleContext.getImageData(0, 0, columns, rows).data\n      const steps = Math.max(2, Math.round(posterize))\n      const luminanceField = new Float32Array(columns * rows)\n      for (let index = 0; index < luminanceField.length; index++) {\n        const pixel = index * 4\n        const alpha = pixels[pixel + 3]! / 255\n        let luminance = (pixels[pixel]! * 0.2126 + pixels[pixel + 1]! * 0.7152 + pixels[pixel + 2]! * 0.0722) / 255\n        luminance = clamp((luminance - 0.5) * Math.max(0, contrast) + 0.5)\n        luminance = clamp(luminance * brightnessBoost * alpha)\n        luminanceField[index] = luminance <= threshold ? 0 : (luminance - threshold) / Math.max(0.001, 1 - threshold)\n      }\n\n      if (dither === \"floyd-steinberg\") {\n        for (let row = 0; row < rows; row++) {\n          for (let column = 0; column < columns; column++) {\n            const index = row * columns + column\n            const oldValue = clamp(luminanceField[index]!)\n            const quantized = Math.round(oldValue * (steps - 1)) / (steps - 1)\n            const value = oldValue + (quantized - oldValue) * clamp(ditherStrength)\n            const error = oldValue - value\n            luminanceField[index] = value\n            if (column + 1 < columns) luminanceField[index + 1]! += error * 7 / 16\n            if (row + 1 < rows) {\n              if (column > 0) luminanceField[index + columns - 1]! += error * 3 / 16\n              luminanceField[index + columns]! += error * 5 / 16\n              if (column + 1 < columns) luminanceField[index + columns + 1]! += error / 16\n            }\n          }\n        }\n      } else if (dither === \"bayer\") {\n        const matrix = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]\n        for (let row = 0; row < rows; row++) {\n          for (let column = 0; column < columns; column++) {\n            const index = row * columns + column\n            const offset = (matrix[(row % 4) * 4 + column % 4]! / 16 - 0.5) * clamp(ditherStrength) / 4\n            luminanceField[index] = clamp(luminanceField[index]! + offset)\n          }\n        }\n      } else {\n        for (let index = 0; index < luminanceField.length; index++) {\n          luminanceField[index] = Math.round(clamp(luminanceField[index]!) * (steps - 1)) / (steps - 1)\n        }\n      }\n\n      const reveal = variant === \"glitch\" && !reduceMotion && revealDuration > 0\n        ? clamp((now - startedAt) / revealDuration)\n        : 1\n\n      updateGlitch(now, rows)\n      if (now > glitchUntil) glitchBands.clear()\n\n      context.fillStyle = backgroundColor\n      context.fillRect(0, 0, width, height)\n      context.textBaseline = \"top\"\n\n      for (let row = 0; row < rows; row++) {\n        const rowOffset = glitchBands.get(row) ?? 0\n        for (let column = 0; column < columns; column++) {\n          const dx = column / Math.max(1, columns - 1) - 0.5\n          const dy = row / Math.max(1, rows - 1) - 0.5\n          if (Math.hypot(dx, dy) > reveal * 0.72) continue\n\n          let sourceColumn = column\n          let sourceRow = row\n          let mouseInfluence = 0\n          if (variant === \"flow\" && !reduceMotion) {\n            const x = column * cellWidth\n            const y = row * cellHeight\n            const phase = (x * directionX + y * directionY) * flowFrequency + now * flowSpeed * Math.PI * 0.002\n            const crossPhase = (-x * directionY + y * directionX) * flowFrequency * 0.65\n            const drift = (Math.sin(phase) + Math.sin(phase * 0.61 + crossPhase) * 0.45) * flowStrength\n            let mouseDisplacement = 0\n\n            if (pointer.current.active && mouseRadius > 0) {\n              const mouseX = x - pointer.current.x\n              const mouseY = y - pointer.current.y\n              const distance = Math.hypot(mouseX, mouseY)\n              mouseInfluence = clamp(1 - distance / mouseRadius)\n              if (distance > 0 && mouseInfluence > 0) {\n                const ripple = Math.sin(distance * 0.055 - now * mouseWaveSpeed * Math.PI * 0.002)\n                mouseDisplacement = mouseInfluence ** 2 * mouseStrength * ripple\n                sourceColumn -= mouseX / distance * mouseDisplacement / cellWidth\n                sourceRow -= mouseY / distance * mouseDisplacement / cellHeight\n              }\n            }\n\n            sourceColumn -= directionX * drift / cellWidth\n            sourceRow -= directionY * drift / cellHeight\n          }\n\n          const sampledColumn = Math.round(clamp(sourceColumn, 0, columns - 1))\n          const sampledRow = Math.round(clamp(sourceRow, 0, rows - 1))\n          const sampleIndex = sampledRow * columns + sampledColumn\n          const pixel = sampleIndex * 4\n          let luminance = clamp(luminanceField[sampleIndex]! + mouseInfluence * 0.08)\n          if (invert) luminance = 1 - luminance\n\n          const character = chars[Math.min(chars.length - 1, Math.floor(luminance * (chars.length - 1)))]\n          if (!character?.trim()) continue\n\n          context.fillStyle = colorMode === \"source\"\n            ? `rgb(${pixels[pixel]}, ${pixels[pixel + 1]}, ${pixels[pixel + 2]})`\n            : gradientColor(colors, luminance)\n          context.fillText(character, column * cellWidth - cellWidth + rowOffset, row * cellHeight - cellHeight)\n        }\n      }\n    }\n\n    const animate = (now: number) => {\n      draw(now)\n      if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n    }\n\n    const start = () => {\n      if (loaded) return\n      loaded = true\n      startedAt = performance.now()\n      resize()\n      if (!reduceMotion && variant !== \"image\") frame = requestAnimationFrame(animate)\n    }\n    image.onload = start\n    image.src = imageSrc\n    if (image.complete) start()\n\n    const observer = new ResizeObserver(resize)\n    observer.observe(container)\n\n    return () => {\n      cancelAnimationFrame(frame)\n      observer.disconnect()\n      image.onload = null\n    }\n  }, [backgroundColor, brightnessBoost, characterSpacing, chars, colorMode, colors, contrast, dither, ditherStrength, fit, flowDirection, flowFrequency, flowSpeed, flowStrength, fontFamily, fontSize, fontWeight, glitchFrequency, glitchIntensity, imageSrc, invert, lineHeight, mouseRadius, mouseStrength, mouseWaveSpeed, posterize, revealDuration, scale, threshold, variant])\n\n  const trackPointer = (event: PointerEvent<HTMLDivElement>) => {\n    onPointerMove?.(event)\n    if (variant !== \"flow\") return\n    const rect = event.currentTarget.getBoundingClientRect()\n    const x = event.clientX - rect.left\n    const y = event.clientY - rect.top\n    if (!pointer.current.active) {\n      pointer.current.x = x\n      pointer.current.y = y\n    }\n    pointer.current.active = true\n    pointer.current.targetX = x\n    pointer.current.targetY = y\n  }\n\n  const resetPointer = (event: PointerEvent<HTMLDivElement>) => {\n    onPointerLeave?.(event)\n    pointer.current.active = false\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative size-full overflow-hidden\", className)}\n      onPointerMove={trackPointer}\n      onPointerLeave={resetPointer}\n      {...props}\n    >\n      <canvas ref={canvasRef} role=\"img\" aria-label={alt} className=\"block size-full\" />\n    </div>\n  )\n}\n\nexport function AsciiImage(props: AsciiPresetProps) {\n  return <AsciiEffect {...props} variant=\"image\" colors={props.colors ?? IMAGE_COLORS} />\n}\n\nexport function AsciiFlow(props: AsciiPresetProps) {\n  return <AsciiEffect {...props} variant=\"flow\" colors={props.colors ?? FLOW_COLORS} />\n}\n\nexport function AsciiGlitch(props: AsciiPresetProps) {\n  return <AsciiEffect {...props} variant=\"glitch\" colors={props.colors ?? GLITCH_COLORS} />\n}\n",
      "type": "registry:ui"
    }
  ]
}
