{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlexCarousel-TS-TW",
	"title": "FlexCarousel",
	"description": "An infinite image row that flows through invisible liquid glass at its edges.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FlexCarousel/FlexCarousel.tsx",
			"content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { CSSProperties } from 'react';\nimport { Renderer, Program, Mesh, Triangle, Plane, Texture, RenderTarget } from 'ogl';\n\ntype BendPreset = 'liquid' | 'ribbon' | 'vortex' | 'arch';\ntype CurlMode = 'twist' | 'rise' | 'fall';\ntype CarouselIntro = 'rise' | 'bloom' | 'spin' | 'deal' | 'none';\ntype CardFit = 'natural' | 'portrait' | 'square' | 'landscape';\n\nexport interface FlexCarouselItem {\n  src: string;\n  alt?: string;\n  title?: string;\n  subtitle?: string;\n}\n\ninterface PresetValues {\n  lensWidth: number;\n  lensHeight: number;\n  tilt: number;\n  roundness: number;\n  bend: number;\n  reach: number;\n  curl: CurlMode;\n  dispersion: number;\n  liquid: number;\n  followCursor: boolean;\n}\n\nexport interface FlexCarouselProps extends Partial<PresetValues> {\n  items?: FlexCarouselItem[];\n  preset?: BendPreset;\n  intro?: CarouselIntro;\n  cardHeight?: number;\n  gap?: number;\n  radius?: number;\n  fit?: CardFit;\n  squeeze?: number;\n  focusOnClick?: boolean;\n  autoplay?: boolean;\n  interval?: number;\n  captions?: boolean;\n  captureWheel?: boolean;\n  onChange?: (index: number, item: FlexCarouselItem) => void;\n  onSelect?: (index: number, item: FlexCarouselItem) => void;\n  className?: string;\n  style?: CSSProperties;\n}\n\ninterface Settings extends PresetValues {\n  intro: CarouselIntro;\n  cardHeight: number;\n  gap: number;\n  radius: number;\n  fit: CardFit;\n  squeeze: number;\n  focusOnClick: boolean;\n  autoplay: boolean;\n  interval: number;\n  captureWheel: boolean;\n}\n\ninterface Slot {\n  item: FlexCarouselItem;\n  index: number;\n  texture: Texture;\n  aspect: number;\n  loaded: boolean;\n  failed: boolean;\n  ready: number;\n  color: number[];\n  image: number[];\n  dispose: () => void;\n}\n\ninterface Metrics {\n  cardH: number;\n  widths: number[];\n  centers: number[];\n  gap: number;\n  loop: number;\n}\n\ninterface Instance {\n  index: number;\n  x0: number;\n  x1: number;\n  y0: number;\n  y1: number;\n}\n\ninterface CardEffect {\n  alpha: number;\n  x: number;\n  y: number;\n  scale: number;\n}\n\ninterface IntroEffects {\n  sceneAlpha: number;\n  strength: number;\n  card: ((rel: number) => CardEffect) | null;\n}\n\ninterface Draw {\n  i: number;\n  rel: number;\n  x: number;\n  y: number;\n  cw: number;\n  ch: number;\n  alpha: number;\n}\n\ninterface Engine {\n  wake: () => void;\n  setItems: (items: FlexCarouselItem[]) => void;\n}\n\ninterface Callbacks {\n  onChange?: (index: number, item: FlexCarouselItem) => void;\n  onSelect?: (index: number, item: FlexCarouselItem) => void;\n}\n\nconst photo = (id: string) => `https://images.unsplash.com/${id}?w=1200&q=80&auto=format&fit=max`;\n\nconst DEFAULT_ITEMS: FlexCarouselItem[] = [\n  {\n    src: photo('photo-1737071371043-761e02b1ef95'),\n    alt: 'An iridescent chrome sculpture of a head',\n    title: 'Iridescence'\n  },\n  {\n    src: photo('photo-1693645325828-62cd35c8da8f'),\n    alt: 'A person in dark clothes standing in front of a white backdrop',\n    title: 'White Room'\n  },\n  { src: photo('photo-1739056238917-d89cd05c48d5'), alt: 'A twisted chrome form on black', title: 'Liquid Metal' },\n  { src: photo('photo-1747191092806-c7da11881986'), alt: 'A clay bust shown in profile', title: 'Clay Study' },\n  { src: photo('photo-1605034313761-73ea4a0cfbf3'), alt: 'Black and white low top sneakers', title: 'Low Tops' },\n  { src: photo('photo-1495462911434-be47104d70fa'), alt: 'A woman in a wide black hat', title: 'Brim' },\n  {\n    src: photo('photo-1786835568925-20cd20007ed0'),\n    alt: 'Three swirling shapes in white, gold and silver on grey',\n    title: 'Three Forms'\n  },\n  { src: photo('photo-1770249196550-ff6c641165cb'), alt: 'A chrome humanoid sculpture', title: 'Automaton' },\n  {\n    src: photo('photo-1787181875088-087d612262a7'),\n    alt: 'A black textured sculpture with an opening at its center',\n    title: 'Obsidian'\n  },\n  {\n    src: photo('photo-1770062422885-2dfcbe8fb65b'),\n    alt: 'A woman in a white shirt crouching on a dark set',\n    title: 'Poise'\n  },\n  { src: photo('photo-1726910133626-9b573eca70ff'), alt: 'Black curved shapes stacked like a fan', title: 'Fins' },\n  { src: photo('photo-1604854574958-59047362c165'), alt: 'A white plaster bust on a black table', title: 'Plaster' }\n];\n\nconst BEND_PRESETS: Record<BendPreset, PresetValues> = {\n  liquid: {\n    lensWidth: 0.74,\n    lensHeight: 1.18,\n    tilt: 62,\n    roundness: 1,\n    bend: 0.34,\n    reach: 0.38,\n    curl: 'twist',\n    dispersion: 0.45,\n    liquid: 0,\n    followCursor: false\n  },\n  ribbon: {\n    lensWidth: 0.8,\n    lensHeight: 0.8,\n    tilt: 0,\n    roundness: 1,\n    bend: 0.34,\n    reach: 0.34,\n    curl: 'twist',\n    dispersion: 0.4,\n    liquid: 0,\n    followCursor: false\n  },\n  vortex: {\n    lensWidth: 0.7,\n    lensHeight: 0.95,\n    tilt: 30,\n    roundness: 1,\n    bend: 0.46,\n    reach: 0.3,\n    curl: 'twist',\n    dispersion: 0.5,\n    liquid: 0,\n    followCursor: false\n  },\n  arch: {\n    lensWidth: 0.8,\n    lensHeight: 0.8,\n    tilt: 0,\n    roundness: 1,\n    bend: 0.3,\n    reach: 0.36,\n    curl: 'rise',\n    dispersion: 0.4,\n    liquid: 0,\n    followCursor: false\n  }\n};\n\nconst STYLE = `\n@keyframes flex-carousel-title { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }\n@keyframes flex-carousel-reveal { from { opacity: 0; } to { opacity: 1; } }\n`;\n\nconst FIT_ASPECT: Record<string, number> = { portrait: 0.75, square: 1, landscape: 4 / 3 };\nconst TAPS = 12;\nconst PIXEL_BUDGET = 4.5e6;\nconst INTRO_DURATION: Record<string, number> = { rise: 2.1, bloom: 1.6, spin: 2.2, deal: 1.5, fade: 0.35 };\n\nconst wrap = (value: number, size: number) => ((((value + size / 2) % size) + size) % size) - size / 2;\nconst clamp01 = (value: number) => Math.min(Math.max(value, 0), 1);\nconst easeOut = (value: number) => 1 - Math.pow(1 - clamp01(value), 3);\nconst easeOutQuint = (value: number) => 1 - Math.pow(1 - clamp01(value), 5);\nconst easeInOut = (value: number) => {\n  const t = clamp01(value);\n  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n};\n\nconst cardVertex = `#version 300 es\nin vec3 position;\nin vec2 uv;\nuniform vec4 uRect;\nuniform vec2 uResolution;\nout vec2 vUv;\nout vec2 vLocal;\nvoid main() {\n  vUv = uv;\n  vLocal = vec2(position.x, -position.y) * uRect.zw;\n  vec2 px = uRect.xy + vLocal;\n  gl_Position = vec4(px.x / uResolution.x * 2.0 - 1.0, 1.0 - px.y / uResolution.y * 2.0, 0.0, 1.0);\n}\n`;\n\nconst cardFragment = `#version 300 es\nprecision highp float;\nuniform sampler2D tMap;\nuniform vec2 uSize;\nuniform vec2 uImage;\nuniform float uRadius;\nuniform float uAlpha;\nuniform float uReady;\nuniform float uShift;\nuniform float uDpr;\nuniform vec3 uPlaceholder;\nin vec2 vUv;\nin vec2 vLocal;\nout vec4 fragColor;\n\nfloat roundedBox(vec2 p, vec2 b, float r) {\n  vec2 q = abs(p) - b + r;\n  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;\n}\n\nvoid main() {\n  float sd = roundedBox(vLocal, uSize * 0.5, min(uRadius, min(uSize.x, uSize.y) * 0.5));\n  float mask = clamp(0.5 - sd * uDpr, 0.0, 1.0);\n  vec2 local = vLocal / uSize + 0.5;\n  float cardAspect = uSize.x / uSize.y;\n  float imageAspect = uImage.x / max(uImage.y, 1.0);\n  vec2 scale = imageAspect > cardAspect ? vec2(cardAspect / imageAspect, 1.0) : vec2(1.0, imageAspect / cardAspect);\n  scale /= 1.08;\n  vec2 uv = vec2(local.x, 1.0 - local.y);\n  uv = (uv - 0.5) * scale + 0.5;\n  uv.x += uShift * (1.0 - scale.x) * 0.5;\n  vec3 image = texture(tMap, uv).rgb;\n  vec3 color = mix(uPlaceholder, image, uReady);\n  float alpha = mask * uAlpha;\n  fragColor = vec4(color * alpha, alpha);\n}\n`;\n\nconst lensVertex = `#version 300 es\nin vec2 position;\nvoid main() {\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst lensFragment = `#version 300 es\nprecision highp float;\nuniform sampler2D tScene;\nuniform vec2 uResolution;\nuniform float uDpr;\nuniform vec2 uCenter;\nuniform vec2 uHalf;\nuniform float uAngle;\nuniform float uExponent;\nuniform float uInner;\nuniform float uOuter;\nuniform float uFlow;\nuniform float uCurl;\nuniform float uDispersion;\nuniform float uStrength;\nuniform float uSceneAlpha;\nout vec4 fragColor;\n\nvoid main() {\n  vec2 frag = gl_FragCoord.xy / uDpr;\n  vec2 uv = frag / uResolution;\n  vec2 rel = frag - vec2(uCenter.x, uResolution.y - uCenter.y);\n  float ca = cos(uAngle);\n  float sa = sin(uAngle);\n  vec2 local = vec2(ca * rel.x + sa * rel.y, -sa * rel.x + ca * rel.y);\n  vec2 k = max(abs(local) / uHalf, vec2(1e-5));\n  float nd = pow(pow(k.x, uExponent) + pow(k.y, uExponent), 1.0 / uExponent);\n  vec2 grad = pow(k, vec2(uExponent - 1.0)) * sign(local) / uHalf * pow(nd, 1.0 - uExponent);\n  float glen = max(length(grad), 1e-6);\n  float edge = (nd - 1.0) / glen;\n  vec2 outward = grad / glen;\n  vec2 normal = vec2(ca * outward.x - sa * outward.y, sa * outward.x + ca * outward.y);\n  vec2 along = vec2(-normal.y, normal.x);\n\n  float t = clamp((edge + uInner) / (uInner + uOuter), 0.0, 1.0);\n  float ramp = t * t * t * (t * (t * 6.0 - 15.0) + 10.0);\n  float slope = 16.0 * t * t * (1.0 - t) * (1.0 - t);\n  float reachX = rel.x / (uResolution.x * 0.5);\n  float side = smoothstep(0.02, 0.3, abs(reachX)) * (uCurl == 0.0 ? sign(reachX) : uCurl);\n  float lift = ramp * side * uFlow * uStrength;\n  vec2 swirl = along * along.y * side * slope * uFlow * uStrength * 0.35;\n  vec2 drift = vec2(0.0, -lift) - swirl;\n  vec2 shifted = uv + drift / uResolution;\n\n  vec2 texels = uResolution * uDpr;\n  vec2 gx = dFdx(shifted);\n  vec2 gy = dFdy(shifted);\n  gx *= min(1.0, 3.0 / max(length(gx * texels), 1e-4));\n  gy *= min(1.0, 3.0 / max(length(gy * texels), 1e-4));\n\n  vec4 color = textureGrad(tScene, shifted, gx, gy);\n  vec2 spread = vec2(0.0, side * slope * uFlow * uStrength) / uResolution * uDispersion;\n  float spreadPx = length(spread * texels);\n  if (color.a > 0.002 && spreadPx > 0.25) {\n    vec3 base = color.rgb / color.a;\n    vec3 sumColor = vec3(0.0);\n    vec3 sumWeight = vec3(0.0);\n    for (int i = 0; i < ${TAPS}; i++) {\n      float s = (float(i) + 0.5) / float(${TAPS});\n      vec4 c = textureGrad(tScene, shifted + spread * (s - 0.5), gx, gy);\n      vec3 w = max(1.0 - abs(vec3(s) - vec3(0.15, 0.5, 0.85)) * 2.6, 0.0) * c.a;\n      sumColor += c.rgb * (w / max(c.a, 0.002));\n      sumWeight += w;\n    }\n    vec3 split = mix(base, sumColor / max(sumWeight, vec3(1e-4)), clamp(sumWeight * 2.0, 0.0, 1.0));\n    color.rgb = mix(color.rgb, clamp(split, 0.0, 1.0) * color.a, smoothstep(0.25, 1.5, spreadPx));\n  }\n\n  fragColor = color * uSceneAlpha;\n}\n`;\n\nconst Digits = ({ value }: { value: number }) => (\n  <span className=\"inline-flex\">\n    {String(value)\n      .padStart(2, '0')\n      .split('')\n      .map((digit, index) => (\n        <span key={index} className=\"inline-block h-[1em] overflow-hidden\">\n          <span\n            className=\"flex flex-col transition-transform duration-[600ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\"\n            style={{ transform: `translateY(${-Number(digit) * 10}%)` }}\n          >\n            {'0123456789'.split('').map(n => (\n              <span key={n} className=\"h-[1em]\">\n                {n}\n              </span>\n            ))}\n          </span>\n        </span>\n      ))}\n  </span>\n);\n\nconst FlexCarousel = ({\n  items = DEFAULT_ITEMS,\n  preset = 'liquid',\n  intro = 'rise',\n  cardHeight = 0.5,\n  gap = 12,\n  radius = 0,\n  fit = 'natural',\n  lensWidth,\n  lensHeight,\n  tilt,\n  roundness,\n  bend,\n  reach,\n  curl,\n  dispersion,\n  liquid,\n  followCursor,\n  squeeze = 0.2,\n  focusOnClick = true,\n  autoplay = false,\n  interval = 4,\n  captions = true,\n  captureWheel = true,\n  onChange,\n  onSelect,\n  className = '',\n  style\n}: FlexCarouselProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const settingsRef = useRef<Settings | null>(null);\n  const itemsRef = useRef<FlexCarouselItem[]>(items);\n  const engineRef = useRef<Engine | null>(null);\n  const callbacksRef = useRef<Callbacks>({ onChange, onSelect });\n  const [active, setActive] = useState(0);\n  const [revealed, setRevealed] = useState(false);\n  const [focusOpen, setFocusOpen] = useState(false);\n\n  const base = BEND_PRESETS[preset] || BEND_PRESETS.liquid;\n  const pick = <K extends keyof PresetValues>(value: PresetValues[K] | undefined, key: K): PresetValues[K] =>\n    value === undefined || value === null ? base[key] : value;\n\n  const list = items && items.length ? items : DEFAULT_ITEMS;\n  const itemsKey = list.map(item => item.src).join('|');\n\n  useEffect(() => {\n    itemsRef.current = list;\n    callbacksRef.current = { onChange, onSelect };\n    settingsRef.current = {\n      intro,\n      cardHeight,\n      gap,\n      radius,\n      fit,\n      lensWidth: pick(lensWidth, 'lensWidth'),\n      lensHeight: pick(lensHeight, 'lensHeight'),\n      tilt: pick(tilt, 'tilt'),\n      roundness: pick(roundness, 'roundness'),\n      bend: pick(bend, 'bend'),\n      reach: pick(reach, 'reach'),\n      curl: pick(curl, 'curl'),\n      dispersion: pick(dispersion, 'dispersion'),\n      liquid: pick(liquid, 'liquid'),\n      followCursor: pick(followCursor, 'followCursor'),\n      squeeze,\n      focusOnClick,\n      autoplay,\n      interval,\n      captureWheel\n    };\n    engineRef.current?.wake();\n  });\n\n  useEffect(() => {\n    engineRef.current?.setItems(itemsRef.current);\n  }, [itemsKey]);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return undefined;\n\n    const renderer = new Renderer({\n      dpr: Math.min(window.devicePixelRatio || 1, 2),\n      alpha: true,\n      premultipliedAlpha: true,\n      antialias: false,\n      depth: false\n    });\n    const gl = renderer.gl;\n    if (!renderer.isWebgl2) {\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      return undefined;\n    }\n    gl.clearColor(0, 0, 0, 0);\n    const canvas = gl.canvas;\n    canvas.style.display = 'block';\n    canvas.style.width = '100%';\n    canvas.style.height = '100%';\n    canvas.setAttribute('aria-hidden', 'true');\n    container.prepend(canvas);\n\n    const cardProgram = new Program(gl, {\n      vertex: cardVertex,\n      fragment: cardFragment,\n      transparent: true,\n      depthTest: false,\n      depthWrite: false,\n      uniforms: {\n        tMap: { value: new Texture(gl) },\n        uRect: { value: [0, 0, 1, 1] },\n        uResolution: { value: [1, 1] },\n        uSize: { value: [1, 1] },\n        uImage: { value: [1, 1] },\n        uRadius: { value: 16 },\n        uAlpha: { value: 1 },\n        uReady: { value: 0 },\n        uShift: { value: 0 },\n        uDpr: { value: 1 },\n        uPlaceholder: { value: [0.5, 0.5, 0.5] }\n      }\n    });\n    cardProgram.setBlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n    const cardMesh = new Mesh(gl, { geometry: new Plane(gl), program: cardProgram });\n\n    const target = new RenderTarget(gl, {\n      width: 2,\n      height: 2,\n      depth: false,\n      minFilter: gl.LINEAR_MIPMAP_LINEAR,\n      magFilter: gl.LINEAR\n    });\n\n    const lensUniforms = {\n      tScene: { value: target.texture },\n      uResolution: { value: [1, 1] },\n      uDpr: { value: 1 },\n      uCenter: { value: [0, 0] },\n      uHalf: { value: [1, 1] },\n      uAngle: { value: 0 },\n      uExponent: { value: 2 },\n      uInner: { value: 60 },\n      uOuter: { value: 80 },\n      uFlow: { value: 0 },\n      uCurl: { value: 0 },\n      uDispersion: { value: 0 },\n      uStrength: { value: 0 },\n      uSceneAlpha: { value: 0 }\n    };\n    const lensMesh = new Mesh(gl, {\n      geometry: new Triangle(gl),\n      program: new Program(gl, {\n        vertex: lensVertex,\n        fragment: lensFragment,\n        uniforms: lensUniforms,\n        depthTest: false,\n        depthWrite: false\n      })\n    });\n\n    const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n    const anisotropy = renderer.getExtension('EXT_texture_filter_anisotropic') ? 8 : 0;\n\n    let slots: Slot[] = [];\n    let width = 1;\n    let height = 1;\n    let pos = 0;\n    let vel = 0;\n    let goal = 0;\n    let mode = 'spring';\n    let wheelAt = 0;\n    let raf = 0;\n    let last = performance.now();\n    let visible = true;\n    let alive = true;\n    let dirty = true;\n    let activeIndex = -1;\n    let interactedAt = -Infinity;\n    let autoplayAt = performance.now();\n    let hasFocus = false;\n    let deform = 0;\n    let deformVel = 0;\n    let layout: Metrics | null = null;\n    let resnap = false;\n    let hover = '';\n    let lift = 1;\n    let energy = 0;\n    let lastPos = 0;\n    const lens = { x: 0, y: 0, vx: 0, vy: 0, ready: false };\n    const pointer = {\n      x: 0,\n      y: 0,\n      over: false,\n      down: false,\n      id: -1,\n      startX: 0,\n      startY: 0,\n      startPos: 0,\n      dragging: false,\n      touch: false,\n      samples: [] as { x: number; t: number }[]\n    };\n    const introState = { kind: 'none', t: 0, running: false, done: false, readyAt: 0 };\n    const focus = { index: -1, pending: -1, t: 0, v: 0, target: 0 };\n    let instances: Instance[] = [];\n\n    const loadSlot = (item: FlexCarouselItem, index: number): Slot => {\n      const texture = new Texture(gl, {\n        generateMipmaps: true,\n        minFilter: gl.LINEAR_MIPMAP_LINEAR,\n        magFilter: gl.LINEAR,\n        anisotropy\n      });\n      const slot: Slot = {\n        item,\n        index,\n        texture,\n        aspect: 0.8,\n        loaded: false,\n        failed: false,\n        ready: 0,\n        color: [0.5, 0.5, 0.5],\n        image: [1, 1],\n        dispose: () => {}\n      };\n      const image = new Image();\n      image.crossOrigin = 'anonymous';\n      image.decoding = 'async';\n      image.onload = () => {\n        if (!alive || !slots.includes(slot)) return;\n        texture.image = image;\n        texture.update();\n        slot.image = [image.naturalWidth || 1, image.naturalHeight || 1];\n        slot.aspect = slot.image[0] / slot.image[1];\n        try {\n          const probe = document.createElement('canvas');\n          probe.width = 8;\n          probe.height = 8;\n          const ctx = probe.getContext('2d', { willReadFrequently: true });\n          if (ctx) {\n            ctx.drawImage(image, 0, 0, 8, 8);\n            const data = ctx.getImageData(0, 0, 8, 8).data;\n            const avg = [0, 0, 0];\n            for (let i = 0; i < data.length; i += 4) {\n              avg[0] += data[i];\n              avg[1] += data[i + 1];\n              avg[2] += data[i + 2];\n            }\n            slot.color = avg.map(v => v / 64 / 255);\n          }\n        } catch {\n          slot.color = [0.5, 0.5, 0.5];\n        }\n        slot.loaded = true;\n        dirty = true;\n        start();\n      };\n      image.onerror = () => {\n        if (!alive) return;\n        slot.failed = true;\n        dirty = true;\n        start();\n      };\n      image.src = item.src;\n      slot.dispose = () => {\n        image.onload = null;\n        image.onerror = null;\n        gl.deleteTexture(texture.texture);\n      };\n      return slot;\n    };\n\n    const setItems = (next: FlexCarouselItem[]) => {\n      slots.forEach(slot => slot.dispose());\n      slots = next.map(loadSlot);\n      activeIndex = -1;\n      layout = null;\n      resnap = true;\n      focus.target = 0;\n      focus.t = 0;\n      focus.v = 0;\n      focus.pending = -1;\n      setFocusOpen(false);\n      introState.readyAt = performance.now();\n      dirty = true;\n      start();\n    };\n\n    const metrics = (s: Settings): Metrics => {\n      const cardH = Math.max(24, s.cardHeight * height);\n      const fixed = FIT_ASPECT[s.fit];\n      const widths = slots.map(slot => (fixed || slot.aspect) * cardH);\n      const centers: number[] = [];\n      let cursor = 0;\n      for (let i = 0; i < widths.length; i++) {\n        centers.push(cursor + widths[i] / 2);\n        cursor += widths[i] + s.gap;\n      }\n      return { cardH, widths, centers, gap: s.gap, loop: Math.max(cursor, 1) };\n    };\n\n    const nearest = (m: Metrics, at: number) => {\n      let best = 0;\n      let bestDist = Infinity;\n      for (let i = 0; i < m.centers.length; i++) {\n        const dist = Math.abs(wrap(m.centers[i] - at, m.loop));\n        if (dist < bestDist) {\n          bestDist = dist;\n          best = i;\n        }\n      }\n      return best;\n    };\n\n    const snapPoint = (m: Metrics, at: number) => {\n      const i = nearest(m, at);\n      return at + wrap(m.centers[i] - at, m.loop);\n    };\n\n    const remap = (from: Metrics, to: Metrics, at: number) => {\n      const i = nearest(from, at);\n      const offset = wrap(at - from.centers[i], from.loop);\n      const cycles = Math.round((at - offset - from.centers[i]) / from.loop);\n      return cycles * to.loop + to.centers[i] + offset * (to.widths[i] / from.widths[i]);\n    };\n\n    const step = (m: Metrics, delta: number) => {\n      let at = snapPoint(m, goal);\n      let index = nearest(m, at);\n      const n = m.centers.length;\n      for (let k = 0; k < Math.abs(delta); k++) {\n        const next = (index + (delta > 0 ? 1 : n - 1)) % n;\n        const distance =\n          delta > 0\n            ? m.widths[index] / 2 + m.gap + m.widths[next] / 2\n            : -(m.widths[next] / 2 + m.gap + m.widths[index] / 2);\n        at += distance;\n        index = next;\n      }\n      goal = at;\n      mode = 'spring';\n      dirty = true;\n      start();\n    };\n\n    const goTo = (m: Metrics, index: number) => {\n      const i = ((index % m.centers.length) + m.centers.length) % m.centers.length;\n      goal = goal + wrap(m.centers[i] - goal, m.loop);\n      mode = 'spring';\n      dirty = true;\n      start();\n    };\n\n    const openFocus = (index: number) => {\n      focus.index = index;\n      focus.pending = -1;\n      focus.target = 1;\n      setFocusOpen(true);\n      dirty = true;\n      start();\n    };\n\n    const closeFocus = () => {\n      focus.pending = -1;\n      if (focus.target === 0) return false;\n      focus.target = 0;\n      setFocusOpen(false);\n      dirty = true;\n      start();\n      return true;\n    };\n\n    const skipIntro = () => {\n      if (introState.running) introState.t = 1;\n    };\n\n    const introEffects = () => {\n      const t = introState.running ? introState.t : introState.done ? 1 : 0;\n      const e: IntroEffects = { sceneAlpha: 1, strength: 1, card: null };\n      if (!introState.done && !introState.running) {\n        e.sceneAlpha = 0;\n        e.strength = 0;\n        return e;\n      }\n      if (t >= 1) return e;\n      const kind = introState.kind;\n      if (kind === 'rise') {\n        e.strength = easeInOut((t - 0.3) / 0.65);\n        e.card = rel => {\n          const delay = Math.min(Math.abs(rel) / (width * 0.6), 1) * 0.34;\n          const local = clamp01((t - delay) / 0.6);\n          return {\n            alpha: clamp01(local * 4),\n            x: 0,\n            y: (1 - easeOutQuint(local)) * height * 0.62,\n            scale: 0.5 + 0.5 * easeInOut((local - 0.18) / 0.82)\n          };\n        };\n      } else if (kind === 'bloom') {\n        e.strength = easeInOut((t - 0.2) / 0.8);\n        e.card = rel => {\n          const delay = Math.min(Math.abs(rel) / (width * 0.6), 1) * 0.25;\n          const local = easeOut((t - delay) / 0.55);\n          return { alpha: local, x: 0, y: 0, scale: 0.92 + 0.08 * local };\n        };\n      } else if (kind === 'spin') {\n        e.sceneAlpha = easeOut(t / 0.25);\n        e.strength = easeOut((t - 0.55) / 0.45);\n      } else if (kind === 'deal') {\n        e.strength = easeOut((t - 0.45) / 0.5);\n        e.card = rel => {\n          const spread = Math.min(Math.abs(rel) / (width * 0.6), 1) * 0.3;\n          const local = easeOut((t - 0.12 - spread) / 0.5);\n          return { alpha: easeOut((t - spread) / 0.12), x: -rel * (1 - local), y: 0, scale: 1 };\n        };\n      } else {\n        e.sceneAlpha = easeOut(t);\n        e.strength = easeOut(t);\n      }\n      return e;\n    };\n\n    const beginIntro = (s: Settings, m: Metrics) => {\n      const kind = reducedMotion && s.intro !== 'none' ? 'fade' : s.intro;\n      introState.kind = INTRO_DURATION[kind] ? kind : 'none';\n      introState.running = introState.kind !== 'none';\n      introState.done = !introState.running;\n      introState.t = 0;\n      if (introState.done) setRevealed(true);\n      if (introState.kind === 'spin') {\n        const distance = m.loop * 1.6 + width;\n        pos = goal + distance;\n        vel = -distance * 3;\n        mode = 'spring';\n      }\n    };\n\n    let resizePending = false;\n    const applyResize = () => {\n      width = Math.max(1, container.clientWidth);\n      height = Math.max(1, container.clientHeight);\n      renderer.dpr = Math.min(window.devicePixelRatio || 1, 2, Math.sqrt(PIXEL_BUDGET / (width * height)));\n      renderer.setSize(width, height);\n      target.setSize(Math.max(2, Math.round(width * renderer.dpr)), Math.max(2, Math.round(height * renderer.dpr)));\n      lensUniforms.tScene.value = target.texture;\n      dirty = true;\n    };\n\n    const resize = () => {\n      resizePending = true;\n      start();\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (resizePending) {\n        resizePending = false;\n        applyResize();\n      }\n      if (!alive) return;\n      const s = settingsRef.current;\n      const dt = Math.min(0.05, Math.max(0.001, (now - last) / 1000));\n      last = now;\n      if (!s || !slots.length) {\n        if (visible) raf = requestAnimationFrame(frame);\n        return;\n      }\n\n      const m = metrics(s);\n      const n = slots.length;\n      let animating = false;\n\n      if (resnap) {\n        goal = snapPoint(m, goal);\n        pos = goal;\n        vel = 0;\n        resnap = false;\n      } else if (layout && layout.loop !== m.loop) {\n        pos = remap(layout, m, pos);\n        goal = remap(layout, m, goal);\n        pointer.startPos = pos + (pointer.x - pointer.startX);\n        animating = true;\n      }\n      layout = m;\n\n      if (!introState.running && !introState.done) {\n        const allSettled = slots.every(slot => slot.loaded || slot.failed);\n        if (allSettled || now - introState.readyAt > 3500) {\n          goal = snapPoint(m, goal);\n          pos = goal;\n          beginIntro(s, m);\n        }\n      }\n      if (introState.running) {\n        introState.t = Math.min(1, introState.t + dt / (INTRO_DURATION[introState.kind] || 1));\n        if (introState.t >= 1) {\n          introState.running = false;\n          introState.done = true;\n          setRevealed(true);\n        }\n        animating = true;\n      }\n\n      if (mode === 'wheel' && now - wheelAt > 150) {\n        goal = snapPoint(m, goal);\n        mode = 'spring';\n      }\n      if (!pointer.dragging) {\n        const spinning = introState.running && introState.kind === 'spin';\n        const stiffness = spinning ? 9 : mode === 'wheel' ? 80 : 55;\n        const damping = 2 * Math.sqrt(stiffness);\n        const steps = Math.ceil(dt / (1 / 240));\n        const h = dt / steps;\n        for (let i = 0; i < steps; i++) {\n          const acc = stiffness * (goal - pos) - damping * vel;\n          vel += acc * h;\n          pos += vel * h;\n        }\n        if (Math.abs(goal - pos) < 0.05 && Math.abs(vel) < 0.5) {\n          pos = goal;\n          vel = 0;\n        } else {\n          animating = true;\n        }\n      } else {\n        animating = true;\n      }\n\n      if (Math.abs(pos) > m.loop * 8) {\n        const shift = Math.round(pos / m.loop) * m.loop;\n        pos -= shift;\n        goal -= shift;\n        pointer.startPos -= shift;\n      }\n\n      const current = nearest(m, pos);\n      if (current !== activeIndex) {\n        activeIndex = current;\n        setActive(current);\n        callbacksRef.current.onChange?.(current, itemsRef.current[current]);\n      }\n\n      if (focus.pending >= 0 && mode === 'spring' && Math.abs(goal - pos) < 1.5 && Math.abs(vel) < 30) {\n        if (current === focus.pending) openFocus(current);\n        else focus.pending = -1;\n      }\n\n      if (\n        s.autoplay &&\n        !reducedMotion &&\n        introState.done &&\n        focus.target === 0 &&\n        focus.t < 0.01 &&\n        !pointer.over &&\n        !pointer.down &&\n        !hasFocus &&\n        mode === 'spring' &&\n        Math.abs(goal - pos) < 1 &&\n        now - interactedAt > 3000 &&\n        now - autoplayAt > s.interval * 1000\n      ) {\n        autoplayAt = now;\n        step(m, 1);\n      }\n      if (s.autoplay && !reducedMotion) animating = true;\n\n      const travel = Math.abs(pos - lastPos) / dt;\n      lastPos = pos;\n      const energyTarget = reducedMotion ? 0 : Math.min(travel / 2600, 1);\n      energy += (energyTarget - energy) * (1 - Math.exp(-dt / (energyTarget > energy ? 0.07 : 0.35)));\n      if (energy > 0.001) animating = true;\n      const liquidAmount = reducedMotion ? 0 : s.liquid;\n      const push = Math.max(-1, Math.min(1, vel / 2200));\n      const deformStiffness = 120;\n      const deformDamping = 2 * Math.sqrt(deformStiffness) * 0.32;\n      deformVel += (deformStiffness * (push - deform) - deformDamping * deformVel) * dt;\n      deform += deformVel * dt;\n      if (Math.abs(deform) > 0.0005 || Math.abs(deformVel) > 0.005) animating = true;\n\n      const focusStiffness = 64;\n      focus.v += (focusStiffness * (focus.target - focus.t) - 2 * Math.sqrt(focusStiffness) * focus.v) * dt;\n      focus.t += focus.v * dt;\n      if (Math.abs(focus.target - focus.t) < 0.0005 && Math.abs(focus.v) < 0.001) {\n        focus.t = focus.target;\n        focus.v = 0;\n      } else {\n        animating = true;\n      }\n      const focusAmount = clamp01(focus.t);\n      const focusEase = easeInOut(focusAmount);\n      const focusW = focus.index >= 0 && focus.index < n ? m.widths[focus.index] : m.cardH;\n      const focusScale = Math.max(1, Math.min(1.3, (height * 0.84) / m.cardH, (width * 0.92) / focusW));\n      const nextLift = 1 + (focusScale - 1) * focusEase;\n      if (Math.abs(nextLift - lift) > 0.0005) {\n        lift = nextLift;\n        container.style.setProperty('--flex-carousel-lift', lift.toFixed(4));\n      }\n\n      const effects = introEffects();\n\n      const homeX = width / 2;\n      const homeY = height / 2;\n      const follow = s.followCursor && pointer.over && !pointer.dragging && !pointer.touch && focus.target === 0;\n      const aimX = follow ? pointer.x : homeX;\n      const aimY = follow ? pointer.y : homeY;\n      if (!lens.ready) {\n        lens.x = homeX;\n        lens.y = homeY;\n        lens.ready = true;\n      }\n      const lensK = 110;\n      const lensC = 2 * Math.sqrt(lensK) * 0.8;\n      lens.vx += (lensK * (aimX - lens.x) - lensC * lens.vx) * dt;\n      lens.vy += (lensK * (aimY - lens.y) - lensC * lens.vy) * dt;\n      lens.x += lens.vx * dt;\n      lens.y += lens.vy * dt;\n      if (Math.abs(aimX - lens.x) + Math.abs(aimY - lens.y) > 0.2 || Math.abs(lens.vx) + Math.abs(lens.vy) > 0.5)\n        animating = true;\n\n      const cardH = m.cardH;\n      let halfW = (s.lensWidth * width) / 2;\n      let halfH = (s.lensHeight * width) / 2;\n      const squash = Math.abs(deform) * liquidAmount;\n      halfW *= 1 + squash * 0.16;\n      halfH *= 1 - squash * 0.08;\n      const lensX = lens.x - deform * 14 * liquidAmount;\n\n      for (let i = 0; i < n; i++) {\n        const slot = slots[i];\n        if (slot.loaded && slot.ready < 1) {\n          slot.ready = Math.min(1, slot.ready + dt / 0.45);\n          animating = true;\n        }\n      }\n\n      const waiting = !introState.done;\n      if (dirty || animating || pointer.dragging) {\n        dirty = false;\n        instances = [];\n        const dpr = renderer.dpr;\n        cardProgram.uniforms.uResolution.value = [width, height];\n        cardProgram.uniforms.uDpr.value = dpr;\n        cardProgram.uniforms.uRadius.value = s.radius;\n        const shrink = 1 - clamp01(s.squeeze) * energy;\n        const draws: Draw[] = [];\n        for (let i = 0; i < n; i++) {\n          const w = m.widths[i];\n          const baseRel = wrap(m.centers[i] - pos, m.loop);\n          for (let k = -3; k <= 3; k++) {\n            const rel = baseRel + k * m.loop;\n            if (Math.abs(rel) - w / 2 > width + 40) continue;\n            const fx = effects.card ? effects.card(rel) : null;\n            let x = homeX + rel + (fx ? fx.x : 0);\n            let scale = shrink * (fx ? fx.scale : 1);\n            let alpha = fx ? fx.alpha : 1;\n            if (focusAmount > 0) {\n              if (i === focus.index && Math.abs(rel) < w) {\n                scale *= 1 + (focusScale - 1) * focusEase;\n              } else {\n                const order = Math.min(Math.abs(rel) / width, 1) * 0.25;\n                const part = easeInOut(focusAmount * 1.25 - order);\n                x += Math.sign(rel) * part * width * 0.7;\n                alpha *= 1 - part;\n              }\n            }\n            const cw = w * scale;\n            if (alpha <= 0.001 || x + cw / 2 < -40 || x - cw / 2 > width + 40) continue;\n            draws.push({ i, rel, x, y: homeY + (fx ? fx.y : 0), cw, ch: cardH * scale, alpha });\n          }\n        }\n        draws.sort((a, b) => Math.abs(b.rel) - Math.abs(a.rel));\n        let first = true;\n        for (const draw of draws) {\n          const slot = slots[draw.i];\n          cardProgram.uniforms.tMap.value = slot.texture;\n          cardProgram.uniforms.uRect.value = [draw.x, draw.y, draw.cw + 2, draw.ch + 2];\n          cardProgram.uniforms.uSize.value = [draw.cw, draw.ch];\n          cardProgram.uniforms.uImage.value = slot.image;\n          cardProgram.uniforms.uAlpha.value = draw.alpha;\n          cardProgram.uniforms.uReady.value = slot.ready;\n          cardProgram.uniforms.uShift.value = reducedMotion ? 0 : Math.max(-1, Math.min(1, draw.rel / (width * 0.75)));\n          cardProgram.uniforms.uPlaceholder.value = slot.color;\n          renderer.render({ scene: cardMesh, target, clear: first });\n          first = false;\n          instances.push({\n            index: draw.i,\n            x0: draw.x - draw.cw / 2,\n            x1: draw.x + draw.cw / 2,\n            y0: draw.y - draw.ch / 2,\n            y1: draw.y + draw.ch / 2\n          });\n        }\n        if (first) {\n          renderer.bindFramebuffer(target);\n          gl.viewport(0, 0, target.width, target.height);\n          gl.clear(gl.COLOR_BUFFER_BIT);\n        }\n        renderer.bindFramebuffer();\n        target.texture.bind();\n        gl.generateMipmap(gl.TEXTURE_2D);\n\n        lensUniforms.uResolution.value = [width, height];\n        lensUniforms.uDpr.value = dpr;\n        lensUniforms.uCenter.value = [lensX, lens.y];\n        lensUniforms.uHalf.value = [Math.max(halfW, 1), Math.max(halfH, 1)];\n        lensUniforms.uAngle.value = (s.tilt * Math.PI) / 180;\n        lensUniforms.uExponent.value = 2 + Math.pow(1 - clamp01(s.roundness), 1.5) * 10;\n        const spanW = Math.max(halfW, 1);\n        const spanH = Math.max(halfH, 1);\n        const inner = Math.max(4, s.reach * (spanW + spanH) * 0.5);\n        lensUniforms.uInner.value = inner;\n        lensUniforms.uOuter.value = inner * 1.6;\n        lensUniforms.uFlow.value = s.bend * (spanW + spanH) * 0.45;\n        lensUniforms.uCurl.value = s.curl === 'rise' ? 1 : s.curl === 'fall' ? -1 : 0;\n        lensUniforms.uDispersion.value = s.dispersion * 0.12 * (1 + Math.abs(deform) * liquidAmount * 1.2);\n        lensUniforms.uStrength.value = effects.strength * (1 - focusEase);\n        lensUniforms.uSceneAlpha.value = effects.sceneAlpha;\n        renderer.render({ scene: lensMesh });\n      }\n\n      let nextHover = '';\n      if (pointer.over && !pointer.dragging && introState.done && s.focusOnClick) {\n        const hit = instances.find(\n          inst => pointer.x >= inst.x0 && pointer.x <= inst.x1 && pointer.y >= inst.y0 && pointer.y <= inst.y1\n        );\n        if (focus.target > 0) nextHover = 'close';\n        else if (hit) nextHover = 'open';\n      }\n      if (nextHover !== hover) {\n        hover = nextHover;\n        if (hover) container.setAttribute('data-hover', hover);\n        else container.removeAttribute('data-hover');\n      }\n\n      if (visible && (animating || waiting || dirty || pointer.down)) raf = requestAnimationFrame(frame);\n    };\n\n    const start = () => {\n      if (raf || !visible || !alive) return;\n      last = performance.now();\n      raf = requestAnimationFrame(frame);\n    };\n\n    const localPoint = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      return [e.clientX - rect.left, e.clientY - rect.top];\n    };\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.button !== undefined && e.button > 0) return;\n      skipIntro();\n      const [x, y] = localPoint(e);\n      pointer.down = true;\n      pointer.id = e.pointerId;\n      pointer.touch = e.pointerType === 'touch';\n      pointer.startX = x;\n      pointer.startY = y;\n      pointer.x = x;\n      pointer.y = y;\n      pointer.startPos = pos;\n      pointer.dragging = false;\n      pointer.samples = [{ x, t: performance.now() }];\n      interactedAt = performance.now();\n      if (Math.abs(vel) > 40) {\n        goal = pos;\n        vel = 0;\n      }\n      dirty = true;\n      start();\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      const [x, y] = localPoint(e);\n      pointer.x = x;\n      pointer.y = y;\n      pointer.over = true;\n      if (pointer.down && e.pointerId === pointer.id) {\n        const dx = x - pointer.startX;\n        const dy = y - pointer.startY;\n        const slop = pointer.touch ? 10 : 5;\n        if (!pointer.dragging) {\n          if (pointer.touch && Math.abs(dy) > slop && Math.abs(dy) > Math.abs(dx)) {\n            pointer.down = false;\n            return;\n          }\n          if (Math.abs(dx) > slop) {\n            pointer.dragging = true;\n            pointer.startX = x;\n            pointer.startPos = pos;\n            closeFocus();\n            try {\n              container.setPointerCapture(e.pointerId);\n            } catch {\n              pointer.dragging = true;\n            }\n            container.setAttribute('data-dragging', '');\n          }\n        }\n        if (pointer.dragging) {\n          pos = pointer.startPos - (x - pointer.startX);\n          goal = pos;\n          vel = 0;\n          const now = performance.now();\n          pointer.samples.push({ x, t: now });\n          while (pointer.samples.length > 2 && now - pointer.samples[0].t > 100) pointer.samples.shift();\n        }\n      }\n      dirty = true;\n      start();\n    };\n\n    const onPointerUp = (e: PointerEvent) => {\n      if (!pointer.down || e.pointerId !== pointer.id) return;\n      pointer.down = false;\n      container.removeAttribute('data-dragging');\n      const s = settingsRef.current;\n      if (!s) return;\n      const m = metrics(s);\n      interactedAt = performance.now();\n      if (pointer.dragging) {\n        pointer.dragging = false;\n        const now = performance.now();\n        const first = pointer.samples[0];\n        const lastSample = pointer.samples[pointer.samples.length - 1];\n        let velocity = 0;\n        if (first && lastSample && lastSample.t > first.t && now - lastSample.t < 70) {\n          velocity = -((lastSample.x - first.x) / (lastSample.t - first.t)) * 1000;\n        }\n        vel = velocity;\n        const landing = snapPoint(m, pos + velocity * 0.32);\n        goal = landing;\n        if (Math.abs(velocity) > 400 && Math.abs(landing - pos) < 1) step(m, velocity > 0 ? 1 : -1);\n        mode = 'spring';\n        start();\n        return;\n      }\n      if (closeFocus()) return;\n      const [x, y] = localPoint(e);\n      const hit = instances.find(inst => x >= inst.x0 && x <= inst.x1 && y >= inst.y0 && y <= inst.y1);\n      if (!hit) return;\n      if (hit.index === activeIndex && Math.abs(goal - pos) < 2) {\n        callbacksRef.current.onSelect?.(hit.index, itemsRef.current[hit.index]);\n        if (s.focusOnClick) openFocus(hit.index);\n      } else {\n        const rel = (hit.x0 + hit.x1) / 2 - width / 2;\n        goal = snapPoint(m, pos + rel);\n        mode = 'spring';\n        if (s.focusOnClick) focus.pending = hit.index;\n        start();\n      }\n    };\n\n    const onPointerLeave = () => {\n      pointer.over = false;\n      dirty = true;\n      start();\n    };\n\n    const onPointerCancel = () => {\n      pointer.down = false;\n      pointer.dragging = false;\n      container.removeAttribute('data-dragging');\n      const s = settingsRef.current;\n      if (!s) return;\n      goal = snapPoint(metrics(s), pos);\n      mode = 'spring';\n      start();\n    };\n\n    const onWheel = (e: WheelEvent) => {\n      const s = settingsRef.current;\n      if (!s || e.ctrlKey) return;\n      let dx = e.deltaX;\n      let dy = e.deltaY;\n      if (e.shiftKey && Math.abs(dx) < Math.abs(dy)) {\n        dx = dy;\n        dy = 0;\n      }\n      const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? height : 1;\n      const horizontal = Math.abs(dx) > Math.abs(dy);\n      if (!horizontal && !s.captureWheel) return;\n      e.preventDefault();\n      skipIntro();\n      interactedAt = performance.now();\n      if (closeFocus()) return;\n      const delta = Math.max(-120, Math.min(120, (horizontal ? dx : dy) * unit));\n      goal += delta * 1.25;\n      mode = 'wheel';\n      wheelAt = performance.now();\n      start();\n    };\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      const s = settingsRef.current;\n      if (!s) return;\n      const m = metrics(s);\n      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n        e.preventDefault();\n        skipIntro();\n        closeFocus();\n        interactedAt = performance.now();\n        step(m, 1);\n      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n        e.preventDefault();\n        skipIntro();\n        closeFocus();\n        interactedAt = performance.now();\n        step(m, -1);\n      } else if (e.key === 'Home') {\n        e.preventDefault();\n        closeFocus();\n        goTo(m, 0);\n      } else if (e.key === 'End') {\n        e.preventDefault();\n        closeFocus();\n        goTo(m, slots.length - 1);\n      } else if (e.key === 'Escape') {\n        if (closeFocus()) e.preventDefault();\n      } else if (e.key === 'Enter' || e.key === ' ') {\n        e.preventDefault();\n        if (closeFocus() || activeIndex < 0) return;\n        callbacksRef.current.onSelect?.(activeIndex, itemsRef.current[activeIndex]);\n        if (s.focusOnClick) openFocus(activeIndex);\n      }\n    };\n\n    const onFocus = () => {\n      hasFocus = true;\n    };\n    const onBlur = () => {\n      hasFocus = false;\n    };\n    const onVisibility = () => {\n      if (!document.hidden) start();\n    };\n\n    container.addEventListener('pointerdown', onPointerDown);\n    container.addEventListener('pointermove', onPointerMove);\n    container.addEventListener('pointerup', onPointerUp);\n    container.addEventListener('pointerleave', onPointerLeave);\n    container.addEventListener('pointercancel', onPointerCancel);\n    container.addEventListener('wheel', onWheel, { passive: false });\n    container.addEventListener('keydown', onKeyDown);\n    container.addEventListener('focus', onFocus);\n    container.addEventListener('blur', onBlur);\n    document.addEventListener('visibilitychange', onVisibility);\n\n    const resizeObserver = new ResizeObserver(resize);\n    resizeObserver.observe(container);\n    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      start();\n    });\n    intersectionObserver.observe(container);\n\n    engineRef.current = {\n      wake: () => {\n        dirty = true;\n        start();\n      },\n      setItems\n    };\n\n    resize();\n    setItems(itemsRef.current);\n\n    return () => {\n      alive = false;\n      visible = false;\n      engineRef.current = null;\n      cancelAnimationFrame(raf);\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      container.removeEventListener('pointerdown', onPointerDown);\n      container.removeEventListener('pointermove', onPointerMove);\n      container.removeEventListener('pointerup', onPointerUp);\n      container.removeEventListener('pointerleave', onPointerLeave);\n      container.removeEventListener('pointercancel', onPointerCancel);\n      container.removeEventListener('wheel', onWheel);\n      container.removeEventListener('keydown', onKeyDown);\n      container.removeEventListener('focus', onFocus);\n      container.removeEventListener('blur', onBlur);\n      document.removeEventListener('visibilitychange', onVisibility);\n      slots.forEach(slot => slot.dispose());\n      slots = [];\n      gl.getExtension('WEBGL_lose_context')?.loseContext();\n      if (canvas.parentNode) canvas.parentNode.removeChild(canvas);\n    };\n  }, []);\n\n  const current = list[active] || list[0];\n  const label = current ? current.title || current.alt || `Image ${active + 1}` : '';\n\n  return (\n    <div\n      ref={containerRef}\n      className={`relative h-full w-full cursor-grab touch-pan-y select-none overflow-hidden overscroll-contain outline-none [-webkit-tap-highlight-color:transparent] focus-visible:shadow-[inset_0_0_0_2px_rgba(128,128,140,0.55)] data-[hover=open]:cursor-zoom-in data-[hover=close]:cursor-zoom-out data-[dragging]:cursor-grabbing ${className}`.trim()}\n      style={{ ...style, '--flex-carousel-half': `${Math.min(Math.max(cardHeight, 0.05), 1) * 50}%` } as CSSProperties}\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label=\"Image carousel\"\n      tabIndex={0}\n    >\n      <style>{STYLE}</style>\n      {captions && current && revealed && (\n        <div\n          className=\"pointer-events-none absolute inset-x-0 top-[calc(50%_+_var(--flex-carousel-half)_*_var(--flex-carousel-lift,1)_+_22px)] flex flex-col items-center gap-1.5 px-4 text-center animate-[flex-carousel-reveal_900ms_ease_both] motion-reduce:animate-none\"\n          aria-hidden=\"true\"\n        >\n          <span\n            key={active}\n            className=\"flex max-w-full flex-col items-center text-[15px] font-medium leading-[1.35] animate-[flex-carousel-title_520ms_cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none\"\n          >\n            {current.title || current.alt}\n            {current.subtitle && <span className=\"font-normal opacity-60\">{current.subtitle}</span>}\n          </span>\n          <span\n            className=\"inline-flex items-center gap-[3px] text-[12px] leading-none tabular-nums opacity-50 transition-opacity duration-[400ms] data-[hidden]:opacity-0\"\n            data-hidden={focusOpen ? '' : undefined}\n          >\n            <Digits value={active + 1} />\n            <span className=\"opacity-60\">/</span>\n            <span>{String(list.length).padStart(2, '0')}</span>\n          </span>\n        </div>\n      )}\n      <div className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {`${label}, ${active + 1} of ${list.length}`}\n      </div>\n    </div>\n  );\n};\n\nexport default FlexCarousel;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11"
	]
}