{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlyingPosters-JS-TW",
	"title": "FlyingPosters",
	"description": "3D posters rotate on scroll infinitely.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FlyingPosters/FlyingPosters.jsx",
			"content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Plane, Program, Mesh, Texture } from 'ogl';\n\nconst vertexShader = `\nprecision highp float;\n\nattribute vec3 position;\nattribute vec2 uv;\nattribute vec3 normal;\n\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat3 normalMatrix;\n\nuniform float uPosition;\nuniform float uTime;\nuniform float uSpeed;\nuniform vec3 distortionAxis;\nuniform vec3 rotationAxis;\nuniform float uDistortion;\n\nvarying vec2 vUv;\nvarying vec3 vNormal;\n\nfloat PI = 3.141592653589793238;\nmat4 rotationMatrix(vec3 axis, float angle) {\n    axis = normalize(axis);\n    float s = sin(angle);\n    float c = cos(angle);\n    float oc = 1.0 - c;\n    \n    return mat4(\n      oc * axis.x * axis.x + c,         oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,  0.0,\n      oc * axis.x * axis.y + axis.z * s,oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,  0.0,\n      oc * axis.z * axis.x - axis.y * s,oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c,           0.0,\n      0.0,                              0.0,                                0.0,                                1.0\n    );\n}\n\nvec3 rotate(vec3 v, vec3 axis, float angle) {\n  mat4 m = rotationMatrix(axis, angle);\n  return (m * vec4(v, 1.0)).xyz;\n}\n\nfloat qinticInOut(float t) {\n  return t < 0.5\n    ? 16.0 * pow(t, 5.0)\n    : -0.5 * abs(pow(2.0 * t - 2.0, 5.0)) + 1.0;\n}\n\nvoid main() {\n  vUv = uv;\n  \n  float norm = 0.5;\n  vec3 newpos = position;\n  float offset = (dot(distortionAxis, position) + norm / 2.) / norm;\n  float localprogress = clamp(\n    (fract(uPosition * 5.0 * 0.01) - 0.01 * uDistortion * offset) / (1. - 0.01 * uDistortion),\n    0.,\n    2.\n  );\n  localprogress = qinticInOut(localprogress) * PI;\n  newpos = rotate(newpos, rotationAxis, localprogress);\n\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(newpos, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform vec2 uImageSize;\nuniform vec2 uPlaneSize;\nuniform sampler2D tMap;\n\nvarying vec2 vUv;\n\nvoid main() {\n  vec2 imageSize = uImageSize;\n  vec2 planeSize = uPlaneSize;\n\n  float imageAspect = imageSize.x / imageSize.y;\n  float planeAspect = planeSize.x / planeSize.y;\n  vec2 scale = vec2(1.0, 1.0);\n\n  if (planeAspect > imageAspect) {\n      scale.x = imageAspect / planeAspect;\n  } else {\n      scale.y = planeAspect / imageAspect;\n  }\n\n  vec2 uv = vUv * scale + (1.0 - scale) * 0.5;\n\n  gl_FragColor = texture2D(tMap, uv);\n}\n`;\n\nfunction AutoBind(self, { include, exclude } = {}) {\n  const getAllProperties = object => {\n    const properties = new Set();\n    do {\n      for (const key of Reflect.ownKeys(object)) {\n        properties.add([object, key]);\n      }\n    } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);\n    return properties;\n  };\n\n  const filter = key => {\n    const match = pattern => (typeof pattern === 'string' ? key === pattern : pattern.test(key));\n\n    if (include) return include.some(match);\n    if (exclude) return !exclude.some(match);\n    return true;\n  };\n\n  for (const [object, key] of getAllProperties(self.constructor.prototype)) {\n    if (key === 'constructor' || !filter(key)) continue;\n    const descriptor = Reflect.getOwnPropertyDescriptor(object, key);\n    if (descriptor && typeof descriptor.value === 'function') {\n      self[key] = self[key].bind(self);\n    }\n  }\n  return self;\n}\n\nfunction lerp(p1, p2, t) {\n  return p1 + (p2 - p1) * t;\n}\n\nfunction map(num, min1, max1, min2, max2, round = false) {\n  const num1 = (num - min1) / (max1 - min1);\n  const num2 = num1 * (max2 - min2) + min2;\n  return round ? Math.round(num2) : num2;\n}\n\nclass Media {\n  constructor({ gl, geometry, scene, screen, viewport, image, length, index, planeWidth, planeHeight, distortion }) {\n    this.extra = 0;\n    this.gl = gl;\n    this.geometry = geometry;\n    this.scene = scene;\n    this.screen = screen;\n    this.viewport = viewport;\n    this.image = image;\n    this.length = length;\n    this.index = index;\n    this.planeWidth = planeWidth;\n    this.planeHeight = planeHeight;\n    this.distortion = distortion;\n\n    this.createShader();\n    this.createMesh();\n    this.onResize();\n  }\n\n  createShader() {\n    const texture = new Texture(this.gl, {\n      generateMipmaps: false\n    });\n\n    this.program = new Program(this.gl, {\n      depthTest: false,\n      depthWrite: false,\n      fragment: fragmentShader,\n      vertex: vertexShader,\n      uniforms: {\n        tMap: { value: texture },\n        uPosition: { value: 0 },\n        uPlaneSize: { value: [0, 0] },\n        uImageSize: { value: [0, 0] },\n        uSpeed: { value: 0 },\n        rotationAxis: { value: [0, 1, 0] },\n        distortionAxis: { value: [1, 1, 0] },\n        uDistortion: { value: this.distortion },\n        uViewportSize: { value: [this.viewport.width, this.viewport.height] },\n        uTime: { value: 0 }\n      },\n      cullFace: false\n    });\n\n    const img = new Image();\n    img.crossOrigin = 'anonymous';\n    img.src = this.image;\n    img.onload = () => {\n      texture.image = img;\n      this.program.uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight];\n    };\n  }\n\n  createMesh() {\n    this.plane = new Mesh(this.gl, {\n      geometry: this.geometry,\n      program: this.program\n    });\n    this.plane.setParent(this.scene);\n  }\n\n  setScale() {\n    this.plane.scale.x = (this.viewport.width * this.planeWidth) / this.screen.width;\n    this.plane.scale.y = (this.viewport.height * this.planeHeight) / this.screen.height;\n\n    this.plane.position.x = 0;\n    this.plane.program.uniforms.uPlaneSize.value = [this.plane.scale.x, this.plane.scale.y];\n  }\n\n  onResize({ screen, viewport } = {}) {\n    if (screen) this.screen = screen;\n    if (viewport) {\n      this.viewport = viewport;\n      this.plane.program.uniforms.uViewportSize.value = [this.viewport.width, this.viewport.height];\n    }\n    this.setScale();\n\n    this.padding = 5;\n    this.height = this.plane.scale.y + this.padding;\n    this.heightTotal = this.height * this.length;\n\n    this.y = -this.heightTotal / 2 + (this.index + 0.5) * this.height;\n  }\n\n  update(scroll) {\n    this.plane.position.y = this.y - scroll.current - this.extra;\n\n    const position = map(this.plane.position.y, -this.viewport.height, this.viewport.height, 5, 15);\n\n    this.program.uniforms.uPosition.value = position;\n    this.program.uniforms.uTime.value += 0.04;\n    this.program.uniforms.uSpeed.value = scroll.current;\n\n    const planeHeight = this.plane.scale.y;\n    const viewportHeight = this.viewport.height;\n\n    const topEdge = this.plane.position.y + planeHeight / 2;\n    const bottomEdge = this.plane.position.y - planeHeight / 2;\n\n    if (topEdge < -viewportHeight / 2) {\n      this.extra -= this.heightTotal;\n    } else if (bottomEdge > viewportHeight / 2) {\n      this.extra += this.heightTotal;\n    }\n  }\n}\n\nclass Canvas {\n  constructor({ container, canvas, items, planeWidth, planeHeight, distortion, scrollEase, cameraFov, cameraZ }) {\n    this.container = container;\n    this.canvas = canvas;\n    this.items = items;\n    this.planeWidth = planeWidth;\n    this.planeHeight = planeHeight;\n    this.distortion = distortion;\n    this.scroll = {\n      ease: scrollEase,\n      current: 0,\n      target: 0,\n      last: 0\n    };\n    this.cameraFov = cameraFov;\n    this.cameraZ = cameraZ;\n\n    AutoBind(this);\n\n    this.createRenderer();\n    this.createCamera();\n    this.createScene();\n    this.onResize();\n\n    this.createGeometry();\n    this.createMedias();\n    this.update();\n    this.addEventListeners();\n    this.createPreloader();\n  }\n\n  createRenderer() {\n    this.renderer = new Renderer({\n      canvas: this.canvas,\n      alpha: true,\n      antialias: true,\n      dpr: Math.min(window.devicePixelRatio, 2)\n    });\n    this.gl = this.renderer.gl;\n  }\n\n  createCamera() {\n    this.camera = new Camera(this.gl);\n    this.camera.fov = this.cameraFov;\n    this.camera.position.z = this.cameraZ;\n  }\n\n  createScene() {\n    this.scene = new Transform();\n  }\n\n  createGeometry() {\n    this.planeGeometry = new Plane(this.gl, {\n      heightSegments: 1,\n      widthSegments: 100\n    });\n  }\n\n  createMedias() {\n    this.medias = this.items.map((image, index) => {\n      return new Media({\n        gl: this.gl,\n        geometry: this.planeGeometry,\n        scene: this.scene,\n        screen: this.screen,\n        viewport: this.viewport,\n        image,\n        length: this.items.length,\n        index,\n        planeWidth: this.planeWidth,\n        planeHeight: this.planeHeight,\n        distortion: this.distortion\n      });\n    });\n  }\n\n  createPreloader() {\n    this.loaded = 0;\n    if (!this.items.length) return;\n\n    this.items.forEach(src => {\n      const image = new Image();\n      image.crossOrigin = 'anonymous';\n      image.src = src;\n      image.onload = () => {\n        this.loaded += 1;\n        if (this.loaded === this.items.length) {\n          document.documentElement.classList.remove('loading');\n          document.documentElement.classList.add('loaded');\n        }\n      };\n    });\n  }\n\n  onResize() {\n    const rect = this.container.getBoundingClientRect();\n    this.screen = {\n      width: rect.width,\n      height: rect.height\n    };\n\n    this.renderer.setSize(this.screen.width, this.screen.height);\n\n    this.camera.perspective({\n      aspect: this.gl.canvas.width / this.gl.canvas.height\n    });\n\n    const fov = (this.camera.fov * Math.PI) / 180;\n    const height = 2 * Math.tan(fov / 2) * this.camera.position.z;\n    const width = height * this.camera.aspect;\n\n    this.viewport = { height, width };\n\n    if (this.medias) {\n      this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n    }\n  }\n\n  onTouchDown(e) {\n    this.isDown = true;\n    this.scroll.position = this.scroll.current;\n    this.start = e.touches ? e.touches[0].clientY : e.clientY;\n  }\n\n  onTouchMove(e) {\n    if (!this.isDown) return;\n    const y = e.touches ? e.touches[0].clientY : e.clientY;\n    const distance = (this.start - y) * 0.1;\n    this.scroll.target = this.scroll.position + distance;\n  }\n\n  onTouchUp() {\n    this.isDown = false;\n  }\n\n  onWheel(e) {\n    const speed = e.deltaY;\n    this.scroll.target += speed * 0.005;\n  }\n\n  update() {\n    this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n\n    if (this.medias) {\n      this.medias.forEach(media => media.update(this.scroll));\n    }\n    this.renderer.render({ scene: this.scene, camera: this.camera });\n    this.scroll.last = this.scroll.current;\n    requestAnimationFrame(this.update);\n  }\n\n  addEventListeners() {\n    window.addEventListener('resize', this.onResize);\n    window.addEventListener('wheel', this.onWheel);\n    window.addEventListener('mousewheel', this.onWheel);\n\n    window.addEventListener('mousedown', this.onTouchDown);\n    window.addEventListener('mousemove', this.onTouchMove);\n    window.addEventListener('mouseup', this.onTouchUp);\n\n    window.addEventListener('touchstart', this.onTouchDown);\n    window.addEventListener('touchmove', this.onTouchMove);\n    window.addEventListener('touchend', this.onTouchUp);\n  }\n\n  destroy() {\n    window.removeEventListener('resize', this.onResize);\n    window.removeEventListener('wheel', this.onWheel);\n    window.removeEventListener('mousewheel', this.onWheel);\n\n    window.removeEventListener('mousedown', this.onTouchDown);\n    window.removeEventListener('mousemove', this.onTouchMove);\n    window.removeEventListener('mouseup', this.onTouchUp);\n\n    window.removeEventListener('touchstart', this.onTouchDown);\n    window.removeEventListener('touchmove', this.onTouchMove);\n    window.removeEventListener('touchend', this.onTouchUp);\n  }\n}\n\nexport default function FlyingPosters({\n  items = [],\n  planeWidth = 320,\n  planeHeight = 320,\n  distortion = 3,\n  scrollEase = 0.1,\n  cameraFov = 45,\n  cameraZ = 20,\n  className,\n  ...props\n}) {\n  const containerRef = useRef(null);\n  const canvasRef = useRef(null);\n  const instanceRef = useRef(null);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    instanceRef.current = new Canvas({\n      container: containerRef.current,\n      canvas: canvasRef.current,\n      items,\n      planeWidth,\n      planeHeight,\n      distortion,\n      scrollEase,\n      cameraFov,\n      cameraZ\n    });\n\n    return () => {\n      if (instanceRef.current) {\n        instanceRef.current.destroy();\n        instanceRef.current = null;\n      }\n    };\n  }, [items, planeWidth, planeHeight, distortion, scrollEase, cameraFov, cameraZ]);\n\n  useEffect(() => {\n    if (!canvasRef.current) return;\n\n    const canvasEl = canvasRef.current;\n\n    const handleWheel = e => {\n      e.preventDefault();\n      if (instanceRef.current) {\n        instanceRef.current.onWheel(e);\n      }\n    };\n\n    const handleTouchMove = e => {\n      e.preventDefault();\n    };\n\n    canvasEl.addEventListener('wheel', handleWheel, { passive: false });\n    canvasEl.addEventListener('touchmove', handleTouchMove, { passive: false });\n\n    return () => {\n      canvasEl.removeEventListener('wheel', handleWheel);\n      canvasEl.removeEventListener('touchmove', handleTouchMove);\n    };\n  }, []);\n\n  return (\n    <div ref={containerRef} className={`w-full h-full overflow-hidden relative z-2 ${className}`} {...props}>\n      <canvas ref={canvasRef} className=\"block w-full h-full\" />\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}