{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ASCIIText-TS-CSS",
	"title": "ASCIIText",
	"description": "Renders text with an animated ASCII background for a retro feel.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "ASCIIText/ASCIIText.tsx",
			"content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\n\nimport { useEffect, useRef } from 'react';\nimport * as THREE from 'three';\n\nconst vertexShader = `\nvarying vec2 vUv;\nuniform float uTime;\nuniform float mouse;\nuniform float uEnableWaves;\n\nvoid main() {\n    vUv = uv;\n    float time = uTime * 5.;\n\n    float waveFactor = uEnableWaves;\n\n    vec3 transformed = position;\n\n    transformed.x += sin(time + position.y) * 0.5 * waveFactor;\n    transformed.y += cos(time + position.z) * 0.15 * waveFactor;\n    transformed.z += sin(time + position.x) * waveFactor;\n\n    gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\n}\n`;\n\nconst fragmentShader = `\nvarying vec2 vUv;\nuniform float mouse;\nuniform float uTime;\nuniform sampler2D uTexture;\n\nvoid main() {\n    float time = uTime;\n    vec2 pos = vUv;\n    \n    float move = sin(time + mouse) * 0.01;\n    float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\n    float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\n    float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\n    float a = texture2D(uTexture, pos).a;\n    gl_FragColor = vec4(r, g, b, a);\n}\n`;\n\nfunction map(n: number, start: number, stop: number, start2: number, stop2: number) {\n  return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\n}\n\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\n\ninterface AsciiFilterOptions {\n  fontSize?: number;\n  fontFamily?: string;\n  charset?: string;\n  invert?: boolean;\n}\n\nclass AsciiFilter {\n  renderer!: THREE.WebGLRenderer;\n  domElement: HTMLDivElement;\n  pre: HTMLPreElement;\n  canvas: HTMLCanvasElement;\n  context: CanvasRenderingContext2D | null;\n  deg: number;\n  invert: boolean;\n  fontSize: number;\n  fontFamily: string;\n  charset: string;\n  width: number = 0;\n  height: number = 0;\n  center: { x: number; y: number } = { x: 0, y: 0 };\n  mouse: { x: number; y: number } = { x: 0, y: 0 };\n  cols: number = 0;\n  rows: number = 0;\n\n  constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {\n    this.renderer = renderer;\n    this.domElement = document.createElement('div');\n    this.domElement.style.position = 'absolute';\n    this.domElement.style.top = '0';\n    this.domElement.style.left = '0';\n    this.domElement.style.width = '100%';\n    this.domElement.style.height = '100%';\n\n    this.pre = document.createElement('pre');\n    this.domElement.appendChild(this.pre);\n\n    this.canvas = document.createElement('canvas');\n    this.context = this.canvas.getContext('2d');\n    this.domElement.appendChild(this.canvas);\n\n    this.deg = 0;\n    this.invert = invert ?? true;\n    this.fontSize = fontSize ?? 12;\n    this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\n    this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\n\n    if (this.context) {\n      this.context.imageSmoothingEnabled = false;\n      this.context.imageSmoothingEnabled = false;\n    }\n\n    this.onMouseMove = this.onMouseMove.bind(this);\n    document.addEventListener('mousemove', this.onMouseMove);\n  }\n\n  setSize(width: number, height: number) {\n    this.width = width;\n    this.height = height;\n    this.renderer.setSize(width, height);\n    this.reset();\n\n    this.center = { x: width / 2, y: height / 2 };\n    this.mouse = { x: this.center.x, y: this.center.y };\n  }\n\n  reset() {\n    if (this.context) {\n      this.context.font = `${this.fontSize}px ${this.fontFamily}`;\n      const charWidth = this.context.measureText('A').width;\n\n      this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\n      this.rows = Math.floor(this.height / this.fontSize);\n\n      this.canvas.width = this.cols;\n      this.canvas.height = this.rows;\n      this.pre.style.fontFamily = this.fontFamily;\n      this.pre.style.fontSize = `${this.fontSize}px`;\n      this.pre.style.margin = '0';\n      this.pre.style.padding = '0';\n      this.pre.style.lineHeight = '1em';\n      this.pre.style.position = 'absolute';\n      this.pre.style.left = '50%';\n      this.pre.style.top = '50%';\n      this.pre.style.transform = 'translate(-50%, -50%)';\n      this.pre.style.zIndex = '9';\n      this.pre.style.backgroundAttachment = 'fixed';\n      this.pre.style.mixBlendMode = 'difference';\n    }\n  }\n\n  render(scene: THREE.Scene, camera: THREE.Camera) {\n    this.renderer.render(scene, camera);\n\n    const w = this.canvas.width;\n    const h = this.canvas.height;\n    if (this.context) {\n      this.context.clearRect(0, 0, w, h);\n      this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\n      this.asciify(this.context, w, h);\n      this.hue();\n    }\n  }\n\n  onMouseMove(e: MouseEvent) {\n    this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\n  }\n\n  get dx() {\n    return this.mouse.x - this.center.x;\n  }\n\n  get dy() {\n    return this.mouse.y - this.center.y;\n  }\n\n  hue() {\n    const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\n    this.deg += (deg - this.deg) * 0.075;\n    this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\n  }\n\n  asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {\n    const imgData = ctx.getImageData(0, 0, w, h).data;\n    let str = '';\n    for (let y = 0; y < h; y++) {\n      for (let x = 0; x < w; x++) {\n        const i = x * 4 + y * 4 * w;\n        const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\n\n        if (a === 0) {\n          str += ' ';\n          continue;\n        }\n\n        let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\n        let idx = Math.floor((1 - gray) * (this.charset.length - 1));\n        if (this.invert) idx = this.charset.length - idx - 1;\n        str += this.charset[idx];\n      }\n      str += '\\n';\n    }\n    this.pre.innerHTML = str;\n  }\n\n  dispose() {\n    document.removeEventListener('mousemove', this.onMouseMove);\n  }\n}\n\ninterface CanvasTxtOptions {\n  fontSize?: number;\n  fontFamily?: string;\n  color?: string;\n}\n\nclass CanvasTxt {\n  canvas: HTMLCanvasElement;\n  context: CanvasRenderingContext2D | null;\n  txt: string;\n  fontSize: number;\n  fontFamily: string;\n  color: string;\n  font: string;\n\n  constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {\n    this.canvas = document.createElement('canvas');\n    this.context = this.canvas.getContext('2d');\n    this.txt = txt;\n    this.fontSize = fontSize;\n    this.fontFamily = fontFamily;\n    this.color = color;\n\n    this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\n  }\n\n  resize() {\n    if (this.context) {\n      this.context.font = this.font;\n      const metrics = this.context.measureText(this.txt);\n\n      const textWidth = Math.ceil(metrics.width) + 20;\n      const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\n\n      this.canvas.width = textWidth;\n      this.canvas.height = textHeight;\n    }\n  }\n\n  render() {\n    if (this.context) {\n      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n      this.context.fillStyle = this.color;\n      this.context.font = this.font;\n\n      const metrics = this.context.measureText(this.txt);\n      const yPos = 10 + metrics.actualBoundingBoxAscent;\n\n      this.context.fillText(this.txt, 10, yPos);\n    }\n  }\n\n  get width() {\n    return this.canvas.width;\n  }\n\n  get height() {\n    return this.canvas.height;\n  }\n\n  get texture() {\n    return this.canvas;\n  }\n}\n\ninterface CanvAsciiOptions {\n  text: string;\n  asciiFontSize: number;\n  textFontSize: number;\n  textColor: string;\n  planeBaseHeight: number;\n  enableWaves: boolean;\n}\n\nclass CanvAscii {\n  textString: string;\n  asciiFontSize: number;\n  textFontSize: number;\n  textColor: string;\n  planeBaseHeight: number;\n  container: HTMLElement;\n  width: number;\n  height: number;\n  enableWaves: boolean;\n  camera: THREE.PerspectiveCamera;\n  scene: THREE.Scene;\n  mouse: { x: number; y: number };\n  textCanvas!: CanvasTxt;\n  texture!: THREE.CanvasTexture;\n  geometry: THREE.PlaneGeometry | undefined;\n  material: THREE.ShaderMaterial | undefined;\n  mesh!: THREE.Mesh;\n  renderer!: THREE.WebGLRenderer;\n  filter!: AsciiFilter;\n  center: { x: number; y: number } = { x: 0, y: 0 };\n  animationFrameId: number = 0;\n\n  constructor(\n    { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: CanvAsciiOptions,\n    containerElem: HTMLElement,\n    width: number,\n    height: number\n  ) {\n    this.textString = text;\n    this.asciiFontSize = asciiFontSize;\n    this.textFontSize = textFontSize;\n    this.textColor = textColor;\n    this.planeBaseHeight = planeBaseHeight;\n    this.container = containerElem;\n    this.width = width;\n    this.height = height;\n    this.enableWaves = enableWaves;\n\n    this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\n    this.camera.position.z = 30;\n\n    this.scene = new THREE.Scene();\n    this.mouse = { x: this.width / 2, y: this.height / 2 };\n\n    this.onMouseMove = this.onMouseMove.bind(this);\n  }\n\n  async init() {\n    try {\n      await document.fonts.load('600 200px \"IBM Plex Mono\"');\n      await document.fonts.load('500 12px \"IBM Plex Mono\"');\n    } catch (e) {}\n    await document.fonts.ready;\n    this.setMesh();\n    this.setRenderer();\n  }\n\n  setMesh() {\n    this.textCanvas = new CanvasTxt(this.textString, {\n      fontSize: this.textFontSize,\n      fontFamily: 'IBM Plex Mono',\n      color: this.textColor\n    });\n    this.textCanvas.resize();\n    this.textCanvas.render();\n\n    this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\n    this.texture.minFilter = THREE.NearestFilter;\n\n    const textAspect = this.textCanvas.width / this.textCanvas.height;\n    const baseH = this.planeBaseHeight;\n    const planeW = baseH * textAspect;\n    const planeH = baseH;\n\n    this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\n    this.material = new THREE.ShaderMaterial({\n      vertexShader,\n      fragmentShader,\n      transparent: true,\n      uniforms: {\n        uTime: { value: 0 },\n        mouse: { value: 1.0 },\n        uTexture: { value: this.texture },\n        uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\n      }\n    });\n\n    this.mesh = new THREE.Mesh(this.geometry, this.material);\n    this.scene.add(this.mesh);\n  }\n\n  setRenderer() {\n    this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\n    this.renderer.setPixelRatio(1);\n    this.renderer.setClearColor(0x000000, 0);\n\n    this.filter = new AsciiFilter(this.renderer, {\n      fontFamily: 'IBM Plex Mono',\n      fontSize: this.asciiFontSize,\n      invert: true\n    });\n\n    this.container.appendChild(this.filter.domElement);\n    this.setSize(this.width, this.height);\n\n    this.container.addEventListener('mousemove', this.onMouseMove);\n    this.container.addEventListener('touchmove', this.onMouseMove);\n  }\n\n  setSize(w: number, h: number) {\n    this.width = w;\n    this.height = h;\n\n    this.camera.aspect = w / h;\n    this.camera.updateProjectionMatrix();\n\n    this.filter.setSize(w, h);\n\n    this.center = { x: w / 2, y: h / 2 };\n  }\n\n  load() {\n    this.animate();\n  }\n\n  onMouseMove(evt: MouseEvent | TouchEvent) {\n    const e = (evt as TouchEvent).touches ? (evt as TouchEvent).touches[0] : (evt as MouseEvent);\n    const bounds = this.container.getBoundingClientRect();\n    const x = e.clientX - bounds.left;\n    const y = e.clientY - bounds.top;\n    this.mouse = { x, y };\n  }\n\n  animate() {\n    const animateFrame = () => {\n      this.animationFrameId = requestAnimationFrame(animateFrame);\n      this.render();\n    };\n    animateFrame();\n  }\n\n  render() {\n    const time = new Date().getTime() * 0.001;\n\n    this.textCanvas.render();\n    this.texture.needsUpdate = true;\n\n    (this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);\n\n    this.updateRotation();\n    this.filter.render(this.scene, this.camera);\n  }\n\n  updateRotation() {\n    const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);\n    const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);\n\n    this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\n    this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\n  }\n\n  clear() {\n    this.scene.traverse(object => {\n      const obj = object as unknown as THREE.Mesh;\n      if (!obj.isMesh) return;\n      [obj.material].flat().forEach(material => {\n        material.dispose();\n        Object.keys(material).forEach(key => {\n          const matProp = material[key as keyof typeof material];\n          if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {\n            matProp.dispose();\n          }\n        });\n      });\n      obj.geometry.dispose();\n    });\n    this.scene.clear();\n  }\n\n  dispose() {\n    cancelAnimationFrame(this.animationFrameId);\n    if (this.filter) {\n      this.filter.dispose();\n      if (this.filter.domElement.parentNode) {\n        this.container.removeChild(this.filter.domElement);\n      }\n    }\n    this.container.removeEventListener('mousemove', this.onMouseMove);\n    this.container.removeEventListener('touchmove', this.onMouseMove);\n    this.clear();\n    if (this.renderer) {\n      this.renderer.dispose();\n      this.renderer.forceContextLoss();\n    }\n  }\n}\n\ninterface ASCIITextProps {\n  text?: string;\n  asciiFontSize?: number;\n  textFontSize?: number;\n  textColor?: string;\n  planeBaseHeight?: number;\n  enableWaves?: boolean;\n}\n\nexport default function ASCIIText({\n  text = 'David!',\n  asciiFontSize = 8,\n  textFontSize = 200,\n  textColor = '#fdf9f3',\n  planeBaseHeight = 8,\n  enableWaves = true\n}: ASCIITextProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const asciiRef = useRef<CanvAscii | null>(null);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    let cancelled = false;\n    let observer: IntersectionObserver | null = null;\n    let ro: ResizeObserver | null = null;\n\n    const createAndInit = async (container: HTMLDivElement, w: number, h: number) => {\n      const instance = new CanvAscii(\n        { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\n        container,\n        w,\n        h\n      );\n      await instance.init();\n      return instance;\n    };\n\n    const setup = async () => {\n      const { width, height } = containerRef.current!.getBoundingClientRect();\n\n      if (width === 0 || height === 0) {\n        observer = new IntersectionObserver(\n          async ([entry]) => {\n            if (cancelled) return;\n            if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\n              const { width: w, height: h } = entry.boundingClientRect;\n              observer?.disconnect();\n              observer = null;\n\n              if (!cancelled) {\n                asciiRef.current = await createAndInit(containerRef.current!, w, h);\n                if (!cancelled && asciiRef.current) {\n                  asciiRef.current.load();\n                }\n              }\n            }\n          },\n          { threshold: 0.1 }\n        );\n        observer.observe(containerRef.current!);\n        return;\n      }\n\n      asciiRef.current = await createAndInit(containerRef.current!, width, height);\n      if (!cancelled && asciiRef.current) {\n        asciiRef.current.load();\n\n        ro = new ResizeObserver(entries => {\n          if (!entries[0] || !asciiRef.current) return;\n          const { width: w, height: h } = entries[0].contentRect;\n          if (w > 0 && h > 0) {\n            asciiRef.current.setSize(w, h);\n          }\n        });\n        ro.observe(containerRef.current!);\n      }\n    };\n\n    setup();\n\n    return () => {\n      cancelled = true;\n      if (observer) observer.disconnect();\n      if (ro) ro.disconnect();\n      if (asciiRef.current) {\n        asciiRef.current.dispose();\n        asciiRef.current = null;\n      }\n    };\n  }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"ascii-text-container\"\n      style={{\n        position: 'absolute',\n        width: '100%',\n        height: '100%'\n      }}\n    >\n      <style>{`\n        @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&display=swap');\n\n        .ascii-text-container canvas {\n          position: absolute;\n          left: 0;\n          top: 0;\n          width: 100%;\n          height: 100%;\n          image-rendering: optimizeSpeed;\n          image-rendering: -moz-crisp-edges;\n          image-rendering: -o-crisp-edges;\n          image-rendering: -webkit-optimize-contrast;\n          image-rendering: optimize-contrast;\n          image-rendering: crisp-edges;\n          image-rendering: pixelated;\n        }\n\n        .ascii-text-container pre {\n          margin: 0;\n          user-select: none;\n          padding: 0;\n          line-height: 1em;\n          text-align: left;\n          position: absolute;\n          left: 0;\n          top: 0;\n          background-image: radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%);\n          background-attachment: fixed;\n          -webkit-text-fill-color: transparent;\n          -webkit-background-clip: text;\n          z-index: 9;\n          mix-blend-mode: difference;\n        }\n      `}</style>\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"three@^0.180.0"
	]
}