{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-texture",
  "description": "Helper that builds an animated (GIF/video-capable) Three.js texture for image shaders.",
  "dependencies": [
    "three"
  ],
  "files": [
    {
      "path": "registry/new-york/shaders/animated-texture.ts",
      "content": "/**\n * Helper that builds an animated (GIF/video-capable) Three.js texture for image shaders.\n */\nimport * as THREE from \"three\";\n\nexport type AnimatedTexture = {\n  texture: THREE.Texture;\n  update: (elapsedMs: number) => void;\n  dispose: () => void;\n};\n\ntype Frame = { bitmap: ImageBitmap; durationMs: number };\n\ntype DecodedFrame = {\n  image: {\n    displayWidth: number;\n    displayHeight: number;\n    duration: number | null;\n    close: () => void;\n  };\n};\ntype ImageDecoderLike = {\n  completed: Promise<void>;\n  tracks: { ready: Promise<void>; selectedTrack?: { frameCount: number } };\n  decode: (opts: { frameIndex: number }) => Promise<DecodedFrame>;\n  close: () => void;\n};\ntype ImageDecoderCtor = new (init: {\n  data: BufferSource;\n  type: string;\n}) => ImageDecoderLike;\n\nexport function createAnimatedTexture(url: string): AnimatedTexture {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = 1;\n  canvas.height = 1;\n  const ctx = canvas.getContext(\"2d\");\n\n  const texture = new THREE.CanvasTexture(canvas);\n  texture.colorSpace = THREE.SRGBColorSpace;\n  texture.minFilter = THREE.LinearFilter;\n  texture.magFilter = THREE.LinearFilter;\n  texture.wrapS = THREE.ClampToEdgeWrapping;\n  texture.wrapT = THREE.ClampToEdgeWrapping;\n\n  let frames: Frame[] = [];\n  let totalDuration = 0;\n  let lastIndex = -1;\n  let disposed = false;\n\n  const resizeCanvas = (w: number, h: number) => {\n    if (canvas.width === w && canvas.height === h) return;\n    canvas.width = w;\n    canvas.height = h;\n    texture.dispose();\n  };\n\n  const drawStatic = (source: CanvasImageSource, w: number, h: number) => {\n    if (disposed || !ctx) return;\n    resizeCanvas(w, h);\n    ctx.drawImage(source, 0, 0, w, h);\n    texture.needsUpdate = true;\n  };\n\n  const loadStatic = () => {\n    const img = new Image();\n    img.crossOrigin = \"anonymous\";\n    img.onload = () => drawStatic(img, img.naturalWidth, img.naturalHeight);\n    img.src = url;\n  };\n\n  const ImageDecoderImpl = (\n    globalThis as unknown as { ImageDecoder?: ImageDecoderCtor }\n  ).ImageDecoder;\n\n  if (!ImageDecoderImpl || !ctx) {\n    loadStatic();\n  } else {\n    (async () => {\n      try {\n        const buffer = await (await fetch(url)).arrayBuffer();\n        if (disposed) return;\n        const decoder = new ImageDecoderImpl({\n          data: buffer,\n          type: \"image/gif\",\n        });\n        await decoder.tracks.ready;\n        await decoder.completed;\n        if (disposed) {\n          decoder.close();\n          return;\n        }\n\n        const count = decoder.tracks.selectedTrack?.frameCount ?? 1;\n        const decoded: Frame[] = [];\n        for (let i = 0; i < count; i++) {\n          const { image } = await decoder.decode({ frameIndex: i });\n          const bitmap = await createImageBitmap(\n            image as unknown as ImageBitmapSource,\n          );\n          decoded.push({ bitmap, durationMs: (image.duration ?? 100_000) / 1000 });\n          image.close();\n        }\n        decoder.close();\n        if (disposed) {\n          decoded.forEach((f) => f.bitmap.close());\n          return;\n        }\n\n        frames = decoded;\n        totalDuration = frames.reduce((sum, f) => sum + f.durationMs, 0) || 1;\n        if (frames[0]) {\n          drawStatic(frames[0].bitmap, frames[0].bitmap.width, frames[0].bitmap.height);\n          lastIndex = 0;\n        }\n      } catch {\n        if (!disposed) loadStatic();\n      }\n    })();\n  }\n\n  const update = (elapsedMs: number) => {\n    if (disposed || frames.length <= 1 || !ctx) return;\n    const t = elapsedMs % totalDuration;\n    let acc = 0;\n    let index = 0;\n    for (let i = 0; i < frames.length; i++) {\n      acc += frames[i].durationMs;\n      if (t < acc) {\n        index = i;\n        break;\n      }\n    }\n    if (index === lastIndex) return;\n    lastIndex = index;\n    ctx.drawImage(frames[index].bitmap, 0, 0);\n    texture.needsUpdate = true;\n  };\n\n  const dispose = () => {\n    disposed = true;\n    frames.forEach((f) => f.bitmap.close());\n    frames = [];\n    texture.dispose();\n  };\n\n  return { texture, update, dispose };\n}\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/animated-texture.ts"
    }
  ],
  "type": "registry:block"
}