{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlyingPosters-TS-TW",
	"title": "FlyingPosters",
	"description": "3D posters rotate on scroll infinitely.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FlyingPosters/FlyingPosters.tsx",
			"content": "import { useRef, useEffect } from 'react';\nimport { Renderer, Camera, Transform, Plane, Program, Mesh, Texture, type OGLRenderingContext } from 'ogl';\n\ntype GL = OGLRenderingContext;\ntype OGLProgram = Program;\ntype OGLMesh = Mesh;\ntype OGLTransform = Transform;\ntype OGLPlane = Plane;\n\ninterface ScreenSize {\n  width: number;\n  height: number;\n}\n\ninterface ViewportSize {\n  width: number;\n  height: number;\n}\n\ninterface ScrollState {\n  position?: number;\n  ease: number;\n  current: number;\n  target: number;\n  last: number;\n}\n\ninterface AutoBindOptions {\n  include?: Array<string | RegExp>;\n  exclude?: Array<string | RegExp>;\n}\n\ninterface MediaParams {\n  gl: GL;\n  geometry: OGLPlane;\n  scene: OGLTransform;\n  screen: ScreenSize;\n  viewport: ViewportSize;\n  image: string;\n  length: number;\n  index: number;\n  planeWidth: number;\n  planeHeight: number;\n  distortion: number;\n}\n\ninterface CanvasParams {\n  container: HTMLElement;\n  canvas: HTMLCanvasElement;\n  items: string[];\n  planeWidth: number;\n  planeHeight: number;\n  distortion: number;\n  scrollEase: number;\n  cameraFov: number;\n  cameraZ: number;\n}\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: any, { include, exclude }: AutoBindOptions = {}) {\n  const getAllProperties = (object: any): Set<[any, string | symbol]> => {\n    const properties = new Set<[any, string | symbol]>();\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: string | symbol) => {\n    const match = (pattern: string | RegExp) =>\n      typeof pattern === 'string' ? key === pattern : (pattern as RegExp).test(key.toString());\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: number, p2: number, t: number): number {\n  return p1 + (p2 - p1) * t;\n}\n\nfunction map(num: number, min1: number, max1: number, min2: number, max2: number, round = false): number {\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  gl: GL;\n  geometry: OGLPlane;\n  scene: OGLTransform;\n  screen: ScreenSize;\n  viewport: ViewportSize;\n  image: string;\n  length: number;\n  index: number;\n  planeWidth: number;\n  planeHeight: number;\n  distortion: number;\n\n  program!: OGLProgram;\n  plane!: OGLMesh;\n  extra = 0;\n  padding = 0;\n  height = 0;\n  heightTotal = 0;\n  y = 0;\n\n  constructor({\n    gl,\n    geometry,\n    scene,\n    screen,\n    viewport,\n    image,\n    length,\n    index,\n    planeWidth,\n    planeHeight,\n    distortion\n  }: MediaParams) {\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, { generateMipmaps: false });\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    this.plane.position.x = 0;\n    this.program.uniforms.uPlaneSize.value = [this.plane.scale.x, this.plane.scale.y];\n  }\n\n  onResize({ screen, viewport }: { screen?: ScreenSize; viewport?: ViewportSize } = {}) {\n    if (screen) this.screen = screen;\n    if (viewport) {\n      this.viewport = viewport;\n      this.program.uniforms.uViewportSize.value = [viewport.width, 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    this.y = -this.heightTotal / 2 + (this.index + 0.5) * this.height;\n  }\n\n  update(scroll: ScrollState) {\n    this.plane.position.y = this.y - scroll.current - this.extra;\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    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  container: HTMLElement;\n  canvas: HTMLCanvasElement;\n  items: string[];\n  planeWidth: number;\n  planeHeight: number;\n  distortion: number;\n  scroll: ScrollState;\n  cameraFov: number;\n  cameraZ: number;\n\n  renderer!: Renderer;\n  gl!: GL;\n  camera!: Camera;\n  scene!: OGLTransform;\n  planeGeometry!: OGLPlane;\n  medias!: Media[];\n  screen!: ScreenSize;\n  viewport!: ViewportSize;\n  isDown = false;\n  start = 0;\n  loaded = 0;\n\n  constructor({\n    container,\n    canvas,\n    items,\n    planeWidth,\n    planeHeight,\n    distortion,\n    scrollEase,\n    cameraFov,\n    cameraZ\n  }: CanvasParams) {\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    this.createRenderer();\n    this.createCamera();\n    this.createScene();\n    this.onResize();\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(\n      (image, index) =>\n        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    this.items.forEach(src => {\n      const image = new Image();\n      image.crossOrigin = 'anonymous';\n      image.src = src;\n      image.onload = () => {\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 = { width: rect.width, height: rect.height };\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    this.viewport = { width, height };\n\n    this.medias?.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n  }\n\n  onTouchDown(e: MouseEvent | TouchEvent) {\n    this.isDown = true;\n    this.scroll.position = this.scroll.current;\n    this.start = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY;\n  }\n\n  onTouchMove(e: MouseEvent | TouchEvent) {\n    if (!this.isDown || !this.scroll.position) return;\n    const y = e instanceof TouchEvent ? 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: WheelEvent) {\n    this.scroll.target += e.deltaY * 0.005;\n  }\n\n  update() {\n    this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n    this.medias?.forEach(media => media.update(this.scroll));\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('mousedown', this.onTouchDown);\n    window.addEventListener('mousemove', this.onTouchMove);\n    window.addEventListener('mouseup', this.onTouchUp);\n    window.addEventListener('touchstart', this.onTouchDown as EventListener);\n    window.addEventListener('touchmove', this.onTouchMove as EventListener);\n    window.addEventListener('touchend', this.onTouchUp as EventListener);\n  }\n\n  destroy() {\n    window.removeEventListener('resize', this.onResize);\n    window.removeEventListener('wheel', this.onWheel);\n    window.removeEventListener('mousedown', this.onTouchDown);\n    window.removeEventListener('mousemove', this.onTouchMove);\n    window.removeEventListener('mouseup', this.onTouchUp);\n    window.removeEventListener('touchstart', this.onTouchDown as EventListener);\n    window.removeEventListener('touchmove', this.onTouchMove as EventListener);\n    window.removeEventListener('touchend', this.onTouchUp as EventListener);\n  }\n}\n\ninterface FlyingPostersProps extends React.HTMLAttributes<HTMLDivElement> {\n  items?: string[];\n  planeWidth?: number;\n  planeHeight?: number;\n  distortion?: number;\n  scrollEase?: number;\n  cameraFov?: number;\n  cameraZ?: number;\n}\n\nexport default function FlyingPosters({\n  items = [],\n  planeWidth = 320,\n  planeHeight = 320,\n  distortion = 3,\n  scrollEase = 0.01,\n  cameraFov = 45,\n  cameraZ = 20,\n  className,\n  ...props\n}: FlyingPostersProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const instanceRef = useRef<Canvas | null>(null);\n\n  useEffect(() => {\n    if (!containerRef.current || !canvasRef.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      instanceRef.current?.destroy();\n      instanceRef.current = null;\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: WheelEvent) => {\n      e.preventDefault();\n      if (instanceRef.current) {\n        instanceRef.current.onWheel(e);\n      }\n    };\n\n    const handleTouchMove = (e: TouchEvent) => {\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"
	]
}