{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CircularGallery-TS-CSS",
	"title": "CircularGallery",
	"description": "Circular orbit gallery rotating images.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "CircularGallery.css",
			"target": "@components/CircularGallery.css",
			"content": ".circular-gallery {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  cursor: grab;\n}\n\n.circular-gallery:active {\n  cursor: grabbing;\n}\n\n.circular-gallery:focus-visible {\n  outline: 2px solid #fff;\n  outline-offset: 4px;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "CircularGallery.tsx",
			"content": "import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from 'ogl';\nimport { useEffect, useRef } from 'react';\n\nimport './CircularGallery.css';\n\ntype GL = Renderer['gl'];\n\nfunction debounce<T extends (...args: any[]) => void>(func: T, wait: number) {\n  let timeout: number;\n  return function (this: any, ...args: Parameters<T>) {\n    window.clearTimeout(timeout);\n    timeout = window.setTimeout(() => func.apply(this, args), wait);\n  };\n}\n\nfunction lerp(p1: number, p2: number, t: number): number {\n  return p1 + (p2 - p1) * t;\n}\n\nfunction autoBind(instance: any): void {\n  const proto = Object.getPrototypeOf(instance);\n  Object.getOwnPropertyNames(proto).forEach(key => {\n    if (key !== 'constructor' && typeof instance[key] === 'function') {\n      instance[key] = instance[key].bind(instance);\n    }\n  });\n}\n\nconst DEFAULT_FONT = 'bold 30px Figtree';\n// Figtree is not guaranteed to be available on the host page, so the component\n// loads it on demand whenever the default font is used.\nconst DEFAULT_FONT_URL = 'https://fonts.googleapis.com/css2?family=Figtree:wght@400;700&display=swap';\n\nfunction deriveFontFamilyFromUrl(url: string): string {\n  const fileName = (url.split('/').pop() || 'custom-font').split('?')[0];\n  const base = fileName.replace(/\\.(woff2?|ttf|otf|eot)$/i, '');\n  return base.replace(/[^a-zA-Z0-9-_ ]/g, '').trim() || 'CircularGalleryFont';\n}\n\nasync function loadFontFromStylesheet(url: string): Promise<string> {\n  const response = await fetch(url);\n  if (!response.ok) throw new Error(`Failed to fetch font stylesheet (${response.status})`);\n  const cssText = await response.text();\n  const faceBlocks = cssText.match(/@font-face\\s*{[^}]*}/g) || [];\n  let family: string | null = null;\n  const fontFaces: FontFace[] = [];\n  for (const block of faceBlocks) {\n    const familyMatch = block.match(/font-family:\\s*['\"]?([^;'\"]+)['\"]?/);\n    const urlMatch = block.match(/url\\(\\s*['\"]?([^'\")]+)['\"]?\\s*\\)/);\n    if (!familyMatch || !urlMatch) continue;\n    family = familyMatch[1].trim();\n    const descriptors: FontFaceDescriptors = {};\n    const weightMatch = block.match(/font-weight:\\s*([^;]+);/);\n    const styleMatch = block.match(/font-style:\\s*([^;]+);/);\n    const rangeMatch = block.match(/unicode-range:\\s*([^;]+);/);\n    if (weightMatch) descriptors.weight = weightMatch[1].trim();\n    if (styleMatch) descriptors.style = styleMatch[1].trim();\n    if (rangeMatch) descriptors.unicodeRange = rangeMatch[1].trim();\n    fontFaces.push(new FontFace(family, `url(${urlMatch[1]})`, descriptors));\n  }\n  if (!family) throw new Error('No @font-face rule found in the stylesheet');\n  await Promise.allSettled(\n    fontFaces.map(async face => {\n      await face.load();\n      document.fonts.add(face);\n    })\n  );\n  return family;\n}\n\nasync function loadFontFromFile(url: string): Promise<string> {\n  const family = deriveFontFamilyFromUrl(url);\n  const fontFace = new FontFace(family, `url(${url})`);\n  await fontFace.load();\n  document.fonts.add(fontFace);\n  return family;\n}\n\nasync function loadCustomFont(fontUrl: string): Promise<string> {\n  const isStylesheet = fontUrl.includes('fonts.googleapis.com') || /\\.css(\\?.*)?$/i.test(fontUrl);\n  return isStylesheet ? loadFontFromStylesheet(fontUrl) : loadFontFromFile(fontUrl);\n}\n\n// Loads `fontUrl` (a stylesheet such as a Google Fonts URL, or a direct font\n// file) and returns a canvas-ready font string that keeps the size/weight from\n// `font` but swaps in the freshly loaded family. Falls back to `font` on error.\nasync function resolveFont(font: string, fontUrl?: string): Promise<string> {\n  // Use the bundled Figtree stylesheet when the caller relies on the default\n  // font, otherwise honor the explicit `fontUrl`.\n  const effectiveUrl = fontUrl || (font === DEFAULT_FONT ? DEFAULT_FONT_URL : null);\n  if (!effectiveUrl) {\n    // A custom family was supplied without a URL – make sure it is ready (in\n    // case the host page declares it) before we draw it to the canvas,\n    // otherwise the first paint silently falls back to a system font.\n    if (document.fonts && document.fonts.load) {\n      try {\n        await document.fonts.load(font);\n        await document.fonts.ready;\n      } catch {\n        // Ignore – fall back to whatever the browser provides.\n      }\n    }\n    return font;\n  }\n  try {\n    const family = await loadCustomFont(effectiveUrl);\n    const sizeMatch = font.match(/^\\s*(.*?\\d+px)/);\n    const prefix = sizeMatch ? sizeMatch[1].trim() : 'bold 30px';\n    const resolved = `${prefix} \"${family}\"`;\n    if (document.fonts && document.fonts.load) {\n      try {\n        await document.fonts.load(resolved);\n      } catch {\n        // Ignore – we still attempt to render with the requested font.\n      }\n    }\n    return resolved;\n  } catch (error) {\n    console.error('CircularGallery: unable to load font from', fontUrl, error);\n    return font;\n  }\n}\n\nfunction getFontSize(font: string): number {\n  const match = font.match(/(\\d+)px/);\n  return match ? parseInt(match[1], 10) : 30;\n}\n\nfunction createTextTexture(\n  gl: GL,\n  text: string,\n  font: string = 'bold 30px monospace',\n  color: string = 'black'\n): { texture: Texture; width: number; height: number } {\n  const canvas = document.createElement('canvas');\n  const context = canvas.getContext('2d');\n  if (!context) throw new Error('Could not get 2d context');\n\n  context.font = font;\n  const metrics = context.measureText(text);\n  const textWidth = Math.ceil(metrics.width);\n  const fontSize = getFontSize(font);\n  const textHeight = Math.ceil(fontSize * 1.2);\n\n  canvas.width = textWidth + 20;\n  canvas.height = textHeight + 20;\n\n  context.font = font;\n  context.fillStyle = color;\n  context.textBaseline = 'middle';\n  context.textAlign = 'center';\n  context.clearRect(0, 0, canvas.width, canvas.height);\n  context.fillText(text, canvas.width / 2, canvas.height / 2);\n\n  const texture = new Texture(gl, { generateMipmaps: false });\n  texture.image = canvas;\n  return { texture, width: canvas.width, height: canvas.height };\n}\n\ninterface TitleProps {\n  gl: GL;\n  plane: Mesh;\n  renderer: Renderer;\n  text: string;\n  textColor?: string;\n  font?: string;\n}\n\nclass Title {\n  gl: GL;\n  plane: Mesh;\n  renderer: Renderer;\n  text: string;\n  textColor: string;\n  font: string;\n  mesh!: Mesh;\n\n  constructor({ gl, plane, renderer, text, textColor = '#545050', font = '30px sans-serif' }: TitleProps) {\n    autoBind(this);\n    this.gl = gl;\n    this.plane = plane;\n    this.renderer = renderer;\n    this.text = text;\n    this.textColor = textColor;\n    this.font = font;\n    this.createMesh();\n  }\n\n  createMesh() {\n    const { texture, width, height } = createTextTexture(this.gl, this.text, this.font, this.textColor);\n    const geometry = new Plane(this.gl);\n    const program = new Program(this.gl, {\n      vertex: `\n        attribute vec3 position;\n        attribute vec2 uv;\n        uniform mat4 modelViewMatrix;\n        uniform mat4 projectionMatrix;\n        varying vec2 vUv;\n        void main() {\n          vUv = uv;\n          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n        }\n      `,\n      fragment: `\n        precision highp float;\n        uniform sampler2D tMap;\n        varying vec2 vUv;\n        void main() {\n          vec4 color = texture2D(tMap, vUv);\n          if (color.a < 0.1) discard;\n          gl_FragColor = color;\n        }\n      `,\n      uniforms: { tMap: { value: texture } },\n      transparent: true\n    });\n    this.mesh = new Mesh(this.gl, { geometry, program });\n    const aspect = width / height;\n    const textHeightScaled = this.plane.scale.y * 0.15;\n    const textWidthScaled = textHeightScaled * aspect;\n    this.mesh.scale.set(textWidthScaled, textHeightScaled, 1);\n    this.mesh.position.y = -this.plane.scale.y * 0.5 - textHeightScaled * 0.5 - 0.05;\n    this.mesh.setParent(this.plane);\n  }\n}\n\ninterface ScreenSize {\n  width: number;\n  height: number;\n}\n\ninterface Viewport {\n  width: number;\n  height: number;\n}\n\ninterface MediaProps {\n  geometry: Plane;\n  gl: GL;\n  image: string;\n  index: number;\n  length: number;\n  renderer: Renderer;\n  scene: Transform;\n  screen: ScreenSize;\n  text: string;\n  viewport: Viewport;\n  bend: number;\n  textColor: string;\n  borderRadius?: number;\n  font?: string;\n}\n\nclass Media {\n  extra: number = 0;\n  geometry: Plane;\n  gl: GL;\n  image: string;\n  index: number;\n  length: number;\n  renderer: Renderer;\n  scene: Transform;\n  screen: ScreenSize;\n  text: string;\n  viewport: Viewport;\n  bend: number;\n  textColor: string;\n  borderRadius: number;\n  font?: string;\n  program!: Program;\n  plane!: Mesh;\n  title!: Title;\n  scale!: number;\n  padding!: number;\n  width!: number;\n  widthTotal!: number;\n  x!: number;\n  speed: number = 0;\n  isBefore: boolean = false;\n  isAfter: boolean = false;\n\n  constructor({\n    geometry,\n    gl,\n    image,\n    index,\n    length,\n    renderer,\n    scene,\n    screen,\n    text,\n    viewport,\n    bend,\n    textColor,\n    borderRadius = 0,\n    font\n  }: MediaProps) {\n    this.geometry = geometry;\n    this.gl = gl;\n    this.image = image;\n    this.index = index;\n    this.length = length;\n    this.renderer = renderer;\n    this.scene = scene;\n    this.screen = screen;\n    this.text = text;\n    this.viewport = viewport;\n    this.bend = bend;\n    this.textColor = textColor;\n    this.borderRadius = borderRadius;\n    this.font = font;\n    this.createShader();\n    this.createMesh();\n    this.createTitle();\n    this.onResize();\n  }\n\n  createShader() {\n    const texture = new Texture(this.gl, {\n      generateMipmaps: true\n    });\n    this.program = new Program(this.gl, {\n      depthTest: false,\n      depthWrite: false,\n      vertex: `\n        precision highp float;\n        attribute vec3 position;\n        attribute vec2 uv;\n        uniform mat4 modelViewMatrix;\n        uniform mat4 projectionMatrix;\n        uniform float uTime;\n        uniform float uSpeed;\n        varying vec2 vUv;\n        void main() {\n          vUv = uv;\n          vec3 p = position;\n          p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5) * (0.1 + uSpeed * 0.5);\n          gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);\n        }\n      `,\n      fragment: `\n        precision highp float;\n        uniform vec2 uImageSizes;\n        uniform vec2 uPlaneSizes;\n        uniform sampler2D tMap;\n        uniform float uBorderRadius;\n        varying vec2 vUv;\n        \n        float roundedBoxSDF(vec2 p, vec2 b, float r) {\n          vec2 d = abs(p) - b;\n          return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;\n        }\n        \n        void main() {\n          vec2 ratio = vec2(\n            min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),\n            min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)\n          );\n          vec2 uv = vec2(\n            vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,\n            vUv.y * ratio.y + (1.0 - ratio.y) * 0.5\n          );\n          vec4 color = texture2D(tMap, uv);\n          \n          float d = roundedBoxSDF(vUv - 0.5, vec2(0.5 - uBorderRadius), uBorderRadius);\n          \n          float edgeSmooth = 0.002;\n          float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);\n          \n          gl_FragColor = vec4(color.rgb, alpha);\n        }\n      `,\n      uniforms: {\n        tMap: { value: texture },\n        uPlaneSizes: { value: [0, 0] },\n        uImageSizes: { value: [0, 0] },\n        uSpeed: { value: 0 },\n        uTime: { value: 100 * Math.random() },\n        uBorderRadius: { value: this.borderRadius }\n      },\n      transparent: true\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.uImageSizes.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  createTitle() {\n    this.title = new Title({\n      gl: this.gl,\n      plane: this.plane,\n      renderer: this.renderer,\n      text: this.text,\n      textColor: this.textColor,\n      font: this.font\n    });\n  }\n\n  update(scroll: { current: number; last: number }, direction: 'right' | 'left') {\n    this.plane.position.x = this.x - scroll.current - this.extra;\n\n    const x = this.plane.position.x;\n    const H = this.viewport.width / 2;\n\n    if (this.bend === 0) {\n      this.plane.position.y = 0;\n      this.plane.rotation.z = 0;\n    } else {\n      const B_abs = Math.abs(this.bend);\n      const R = (H * H + B_abs * B_abs) / (2 * B_abs);\n      const effectiveX = Math.min(Math.abs(x), H);\n\n      const arc = R - Math.sqrt(R * R - effectiveX * effectiveX);\n      if (this.bend > 0) {\n        this.plane.position.y = -arc;\n        this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / R);\n      } else {\n        this.plane.position.y = arc;\n        this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / R);\n      }\n    }\n\n    this.speed = scroll.current - scroll.last;\n    this.program.uniforms.uTime.value += 0.04;\n    this.program.uniforms.uSpeed.value = this.speed;\n\n    const planeOffset = this.plane.scale.x / 2;\n    const viewportOffset = this.viewport.width / 2;\n    this.isBefore = this.plane.position.x + planeOffset < -viewportOffset;\n    this.isAfter = this.plane.position.x - planeOffset > viewportOffset;\n    if (direction === 'right' && this.isBefore) {\n      this.extra -= this.widthTotal;\n      this.isBefore = this.isAfter = false;\n    }\n    if (direction === 'left' && this.isAfter) {\n      this.extra += this.widthTotal;\n      this.isBefore = this.isAfter = false;\n    }\n  }\n\n  onResize({ screen, viewport }: { screen?: ScreenSize; viewport?: Viewport } = {}) {\n    if (screen) this.screen = screen;\n    if (viewport) {\n      this.viewport = viewport;\n      if (this.plane.program.uniforms.uViewportSizes) {\n        this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height];\n      }\n    }\n    this.scale = this.screen.height / 1500;\n    this.plane.scale.y = (this.viewport.height * (900 * this.scale)) / this.screen.height;\n    this.plane.scale.x = (this.viewport.width * (700 * this.scale)) / this.screen.width;\n    this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y];\n    this.padding = 2;\n    this.width = this.plane.scale.x + this.padding;\n    this.widthTotal = this.width * this.length;\n    this.x = this.width * this.index;\n  }\n}\n\ninterface AppConfig {\n  items?: { image: string; text: string }[];\n  bend?: number;\n  textColor?: string;\n  borderRadius?: number;\n  font?: string;\n  scrollSpeed?: number;\n  scrollEase?: number;\n}\n\nclass App {\n  container: HTMLElement;\n  scrollSpeed: number;\n  scroll: {\n    ease: number;\n    current: number;\n    target: number;\n    last: number;\n    position?: number;\n  };\n  onCheckDebounce: (...args: any[]) => void;\n  renderer!: Renderer;\n  gl!: GL;\n  camera!: Camera;\n  scene!: Transform;\n  planeGeometry!: Plane;\n  medias: Media[] = [];\n  mediasImages: { image: string; text: string }[] = [];\n  screen!: { width: number; height: number };\n  viewport!: { width: number; height: number };\n  raf: number = 0;\n\n  boundOnResize!: () => void;\n  boundOnWheel!: (e: Event) => void;\n  boundOnTouchDown!: (e: MouseEvent | TouchEvent) => void;\n  boundOnTouchMove!: (e: MouseEvent | TouchEvent) => void;\n  boundOnTouchUp!: () => void;\n  boundOnKeyDown!: (e: KeyboardEvent) => void;\n\n  isDown: boolean = false;\n  start: number = 0;\n\n  constructor(\n    container: HTMLElement,\n    {\n      items,\n      bend = 1,\n      textColor = '#ffffff',\n      borderRadius = 0,\n      font = 'bold 30px Figtree',\n      scrollSpeed = 2,\n      scrollEase = 0.05\n    }: AppConfig\n  ) {\n    document.documentElement.classList.remove('no-js');\n    this.container = container;\n    this.scrollSpeed = scrollSpeed;\n    this.scroll = { ease: scrollEase, current: 0, target: 0, last: 0 };\n    this.onCheckDebounce = debounce(this.onCheck.bind(this), 200);\n    this.createRenderer();\n    this.createCamera();\n    this.createScene();\n    this.onResize();\n    this.createGeometry();\n    this.createMedias(items, bend, textColor, borderRadius, font);\n    this.update();\n    this.addEventListeners();\n  }\n\n  createRenderer() {\n    this.renderer = new Renderer({\n      alpha: true,\n      antialias: true,\n      dpr: Math.min(window.devicePixelRatio || 1, 2)\n    });\n    this.gl = this.renderer.gl;\n    this.gl.clearColor(0, 0, 0, 0);\n    this.container.appendChild(this.renderer.gl.canvas as HTMLCanvasElement);\n  }\n\n  createCamera() {\n    this.camera = new Camera(this.gl);\n    this.camera.fov = 45;\n    this.camera.position.z = 20;\n  }\n\n  createScene() {\n    this.scene = new Transform();\n  }\n\n  createGeometry() {\n    this.planeGeometry = new Plane(this.gl, {\n      heightSegments: 50,\n      widthSegments: 100\n    });\n  }\n\n  createMedias(\n    items: { image: string; text: string }[] | undefined,\n    bend: number = 1,\n    textColor: string,\n    borderRadius: number,\n    font: string\n  ) {\n    const defaultItems = [\n      {\n        image: `https://picsum.photos/seed/1/800/600?grayscale`,\n        text: 'Bridge'\n      },\n      {\n        image: `https://picsum.photos/seed/2/800/600?grayscale`,\n        text: 'Desk Setup'\n      },\n      {\n        image: `https://picsum.photos/seed/3/800/600?grayscale`,\n        text: 'Waterfall'\n      },\n      {\n        image: `https://picsum.photos/seed/4/800/600?grayscale`,\n        text: 'Strawberries'\n      },\n      {\n        image: `https://picsum.photos/seed/5/800/600?grayscale`,\n        text: 'Deep Diving'\n      },\n      {\n        image: `https://picsum.photos/seed/16/800/600?grayscale`,\n        text: 'Train Track'\n      },\n      {\n        image: `https://picsum.photos/seed/17/800/600?grayscale`,\n        text: 'Santorini'\n      },\n      {\n        image: `https://picsum.photos/seed/8/800/600?grayscale`,\n        text: 'Blurry Lights'\n      },\n      {\n        image: `https://picsum.photos/seed/9/800/600?grayscale`,\n        text: 'New York'\n      },\n      {\n        image: `https://picsum.photos/seed/10/800/600?grayscale`,\n        text: 'Good Boy'\n      },\n      {\n        image: `https://picsum.photos/seed/21/800/600?grayscale`,\n        text: 'Coastline'\n      },\n      {\n        image: `https://picsum.photos/seed/12/800/600?grayscale`,\n        text: 'Palm Trees'\n      }\n    ];\n    const galleryItems = items && items.length ? items : defaultItems;\n    this.mediasImages = galleryItems.concat(galleryItems);\n    this.medias = this.mediasImages.map((data, index) => {\n      return new Media({\n        geometry: this.planeGeometry,\n        gl: this.gl,\n        image: data.image,\n        index,\n        length: this.mediasImages.length,\n        renderer: this.renderer,\n        scene: this.scene,\n        screen: this.screen,\n        text: data.text,\n        viewport: this.viewport,\n        bend,\n        textColor,\n        borderRadius,\n        font\n      });\n    });\n  }\n\n  onTouchDown(e: MouseEvent | TouchEvent) {\n    this.isDown = true;\n    this.scroll.position = this.scroll.current;\n    this.start = 'touches' in e ? e.touches[0].clientX : e.clientX;\n  }\n\n  onTouchMove(e: MouseEvent | TouchEvent) {\n    if (!this.isDown) return;\n    const x = 'touches' in e ? e.touches[0].clientX : e.clientX;\n    const distance = (this.start - x) * (this.scrollSpeed * 0.025);\n    this.scroll.target = (this.scroll.position ?? 0) + distance;\n  }\n\n  onTouchUp() {\n    this.isDown = false;\n    this.onCheck();\n  }\n\n  onWheel(e: Event) {\n    const wheelEvent = e as WheelEvent;\n    const delta = wheelEvent.deltaY || (wheelEvent as any).wheelDelta || (wheelEvent as any).detail;\n    this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2;\n    this.onCheckDebounce();\n  }\n\n  onKeyDown(e: KeyboardEvent) {\n    switch (e.key) {\n      case 'ArrowRight':\n        e.preventDefault();\n        this.scroll.target += this.scrollSpeed * 5;\n        this.onCheckDebounce();\n        break;\n\n      case 'ArrowLeft':\n        e.preventDefault();\n        this.scroll.target -= this.scrollSpeed * 5;\n        this.onCheckDebounce();\n        break;\n\n      default:\n        break;\n    }\n  }\n\n  onCheck() {\n    if (!this.medias || !this.medias[0]) return;\n    const width = this.medias[0].width;\n    const itemIndex = Math.round(Math.abs(this.scroll.target) / width);\n    const item = width * itemIndex;\n    this.scroll.target = this.scroll.target < 0 ? -item : item;\n  }\n\n  onResize() {\n    this.screen = {\n      width: this.container.clientWidth,\n      height: this.container.clientHeight\n    };\n    this.renderer.setSize(this.screen.width, this.screen.height);\n    this.camera.perspective({\n      aspect: this.screen.width / this.screen.height\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    if (this.medias) {\n      this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));\n    }\n  }\n\n  update() {\n    this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);\n    const direction = this.scroll.current > this.scroll.last ? 'right' : 'left';\n    if (this.medias) {\n      this.medias.forEach(media => media.update(this.scroll, direction));\n    }\n    this.renderer.render({ scene: this.scene, camera: this.camera });\n    this.scroll.last = this.scroll.current;\n    this.raf = window.requestAnimationFrame(this.update.bind(this));\n  }\n\n  addEventListeners() {\n    this.boundOnResize = this.onResize.bind(this);\n    this.boundOnWheel = this.onWheel.bind(this);\n    this.boundOnTouchDown = this.onTouchDown.bind(this);\n    this.boundOnTouchMove = this.onTouchMove.bind(this);\n    this.boundOnTouchUp = this.onTouchUp.bind(this);\n    this.boundOnKeyDown = this.onKeyDown.bind(this);\n\n    window.addEventListener('resize', this.boundOnResize);\n    window.addEventListener('mousewheel', this.boundOnWheel);\n    window.addEventListener('wheel', this.boundOnWheel);\n    window.addEventListener('mousedown', this.boundOnTouchDown);\n    window.addEventListener('mousemove', this.boundOnTouchMove);\n    window.addEventListener('mouseup', this.boundOnTouchUp);\n    window.addEventListener('touchstart', this.boundOnTouchDown);\n    window.addEventListener('touchmove', this.boundOnTouchMove);\n    window.addEventListener('touchend', this.boundOnTouchUp);\n\n    this.container?.addEventListener('keydown', this.boundOnKeyDown);\n  }\n\n  destroy() {\n    window.cancelAnimationFrame(this.raf);\n    window.removeEventListener('resize', this.boundOnResize);\n    window.removeEventListener('mousewheel', this.boundOnWheel);\n    window.removeEventListener('wheel', this.boundOnWheel);\n    window.removeEventListener('mousedown', this.boundOnTouchDown);\n    window.removeEventListener('mousemove', this.boundOnTouchMove);\n    window.removeEventListener('mouseup', this.boundOnTouchUp);\n    window.removeEventListener('touchstart', this.boundOnTouchDown);\n    window.removeEventListener('touchmove', this.boundOnTouchMove);\n    window.removeEventListener('touchend', this.boundOnTouchUp);\n    if (this.renderer && this.renderer.gl && this.renderer.gl.canvas.parentNode) {\n      this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas as HTMLCanvasElement);\n    }\n    if (this.container) {\n      this.container.removeEventListener('keydown', this.boundOnKeyDown);\n    }\n  }\n}\n\ninterface CircularGalleryProps {\n  items?: { image: string; text: string }[];\n  bend?: number;\n  textColor?: string;\n  borderRadius?: number;\n  font?: string;\n  fontUrl?: string;\n  scrollSpeed?: number;\n  scrollEase?: number;\n}\n\nexport default function CircularGallery({\n  items,\n  bend = 3,\n  textColor = '#ffffff',\n  borderRadius = 0.05,\n  font = 'bold 30px Figtree',\n  fontUrl,\n  scrollSpeed = 2,\n  scrollEase = 0.05\n}: CircularGalleryProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (!containerRef.current) return;\n    let app: App | undefined;\n    let isMounted = true;\n    resolveFont(font, fontUrl).then(resolvedFont => {\n      if (!isMounted || !containerRef.current) return;\n      app = new App(containerRef.current, {\n        items,\n        bend,\n        textColor,\n        borderRadius,\n        font: resolvedFont,\n        scrollSpeed,\n        scrollEase\n      });\n    });\n    return () => {\n      isMounted = false;\n      if (app) app.destroy();\n    };\n  }, [items, bend, textColor, borderRadius, font, fontUrl, scrollSpeed, scrollEase]);\n  return (\n    <div\n      className=\"circular-gallery\"\n      ref={containerRef}\n      tabIndex={0}\n      role=\"region\"\n      aria-label=\"Circular image gallery. Use Left and Right Arrow keys to navigate.\"\n    />\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}