{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "MorphSlider-JS-CSS",
	"title": "MorphSlider",
	"description": "WebGL slider that melts between images with a displacement transition.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "MorphSlider.css",
			"target": "@components/MorphSlider.css",
			"content": ".morph-slider {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  background: #0c0c0e;\n  user-select: none;\n  touch-action: pan-y;\n}\n\n.morph-slider-stage {\n  position: absolute;\n  inset: 0;\n  cursor: grab;\n  outline: none;\n}\n\n.morph-slider-stage:active {\n  cursor: grabbing;\n}\n\n.morph-slider-stage:focus-visible {\n  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.7);\n}\n\n.morph-slider-canvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.morph-slider-caption {\n  position: absolute;\n  left: 22px;\n  bottom: 22px;\n  z-index: 2;\n  max-width: 70%;\n  pointer-events: none;\n  display: grid;\n}\n\n.morph-slider-caption-text {\n  grid-area: 1 / 1;\n  justify-self: start;\n  display: inline-block;\n  padding: 8px 14px;\n  border-radius: 10px;\n  font-size: 15px;\n  font-weight: 600;\n  letter-spacing: 0.01em;\n  color: #fff;\n  background: rgba(10, 10, 12, 0.42);\n  backdrop-filter: blur(8px);\n  -webkit-backdrop-filter: blur(8px);\n  opacity: 0;\n  transform: translateY(12px);\n  filter: blur(6px);\n  transition:\n    opacity var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n    transform var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1),\n    filter var(--ms-swap) cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.morph-slider-caption-text.is-active {\n  opacity: 1;\n  transform: translateY(0);\n  filter: blur(0);\n}\n\n.morph-slider-controls {\n  position: absolute;\n  top: 50%;\n  left: 0;\n  right: 0;\n  z-index: 3;\n  display: flex;\n  justify-content: space-between;\n  padding: 0 16px;\n  transform: translateY(-50%);\n  pointer-events: none;\n}\n\n.morph-slider-btn {\n  pointer-events: auto;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 40px;\n  height: 40px;\n  border: 1px solid rgba(255, 255, 255, 0.22);\n  border-radius: 50%;\n  color: #fff;\n  background: rgba(12, 12, 14, 0.4);\n  backdrop-filter: blur(8px);\n  -webkit-backdrop-filter: blur(8px);\n  cursor: pointer;\n  transition:\n    transform 0.25s cubic-bezier(0.16, 1, 0.3, 1),\n    background 0.25s ease,\n    border-color 0.25s ease;\n}\n\n.morph-slider-btn:hover {\n  background: rgba(24, 24, 28, 0.6);\n  border-color: rgba(255, 255, 255, 0.5);\n  transform: scale(1.06);\n}\n\n.morph-slider-btn:active {\n  transform: scale(0.96);\n}\n\n.morph-slider-btn:focus-visible {\n  outline: 2px solid rgba(255, 255, 255, 0.8);\n  outline-offset: 2px;\n}\n\n.morph-slider-indicators {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 18px;\n  z-index: 3;\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n  align-items: center;\n}\n\n.morph-slider-dot {\n  width: 8px;\n  height: 8px;\n  padding: 0;\n  border: none;\n  border-radius: 999px;\n  background: rgba(255, 255, 255, 0.35);\n  cursor: pointer;\n  transition:\n    width var(--ms-dot) cubic-bezier(0.16, 1, 0.3, 1),\n    background var(--ms-dot) ease;\n}\n\n.morph-slider-dot.is-active {\n  width: 22px;\n  background: rgba(255, 255, 255, 0.95);\n}\n\n.morph-slider-dot:focus-visible {\n  outline: 2px solid rgba(255, 255, 255, 0.8);\n  outline-offset: 2px;\n}\n\n@media (max-width: 420px) {\n  .morph-slider-btn {\n    width: 34px;\n    height: 34px;\n  }\n\n  .morph-slider-caption-text {\n    font-size: 13px;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .morph-slider-caption-text,\n  .morph-slider-btn,\n  .morph-slider-dot {\n    animation: none;\n    transition: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "MorphSlider.jsx",
			"content": "import { useEffect, useRef, useState, useCallback } from 'react';\nimport { Renderer, Triangle, Program, Mesh, Texture } from 'ogl';\nimport { gsap } from 'gsap';\n\nimport './MorphSlider.css';\n\nconst TRANSITIONS = { melt: 0, ripple: 1, shear: 2, swirl: 3 };\n\nconst DEFAULT_ITEMS = [\n  {\n    image: 'https://images.unsplash.com/photo-1782977389500-dd7adad33ebe?q=80&w=1600&auto=format&fit=crop',\n    caption: 'One'\n  },\n  {\n    image: 'https://images.unsplash.com/photo-1781499455083-6ccc3beb20cd?q=80&w=1600&auto=format&fit=crop',\n    caption: 'Two'\n  },\n  {\n    image: 'https://images.unsplash.com/photo-1776394254711-4a0d7345269a?q=80&w=1600&auto=format&fit=crop',\n    caption: 'Three'\n  },\n  {\n    image: 'https://images.unsplash.com/photo-1781242629922-6f39cc3671cd?q=80&w=1600&auto=format&fit=crop',\n    caption: 'Four'\n  }\n];\n\nconst vertexShader = `\nattribute vec2 position;\nattribute vec2 uv;\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `\nprecision highp float;\n\nuniform sampler2D tCurrent;\nuniform sampler2D tNext;\nuniform vec2 uResolution;\nuniform vec2 uCurrentSize;\nuniform vec2 uNextSize;\nuniform float uProgress;\nuniform float uDir;\nuniform int uMode;\nuniform float uIntensity;\nuniform float uScale;\nuniform float uAberration;\nuniform float uDrift;\nuniform float uTime;\nuniform float uReduce;\nuniform vec2 uPointer;\nuniform vec3 uOverlay;\n\nvarying vec2 vUv;\n\nconst float PI = 3.14159265359;\n\nfloat hash11(float p) {\n  p = fract(p * 0.1031);\n  p *= p + 33.33;\n  p *= p + p;\n  return fract(p);\n}\n\nfloat hash21(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  float a = hash21(i);\n  float b = hash21(i + vec2(1.0, 0.0));\n  float c = hash21(i + vec2(0.0, 1.0));\n  float d = hash21(i + vec2(1.0, 1.0));\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.5;\n  for (int i = 0; i < 5; i++) {\n    v += a * noise(p);\n    p *= 2.0;\n    a *= 0.5;\n  }\n  return v;\n}\n\nmat2 rot(float a) {\n  float s = sin(a);\n  float c = cos(a);\n  return mat2(c, -s, s, c);\n}\n\nvec2 coverUV(vec2 uv, vec2 res, vec2 img) {\n  float rA = res.x / max(res.y, 1.0);\n  float iA = img.x / max(img.y, 1.0);\n  vec2 s = vec2(1.0);\n  float ratio = rA / max(iA, 0.0001);\n  if (ratio > 1.0) {\n    s.y = 1.0 / ratio;\n  } else {\n    s.x = ratio;\n  }\n  return (uv - 0.5) * s + 0.5;\n}\n\nvoid main() {\n  float p = clamp(uProgress, 0.0, 1.0);\n  float env = sin(p * PI);\n\n  vec2 uv = vUv;\n\n  uv += vec2(sin(uTime * 0.25 + uv.y * 4.0), cos(uTime * 0.22 + uv.x * 4.0)) * uDrift * 0.008;\n  uv = (uv - 0.5) * (1.0 - uDrift * 0.02 * sin(uTime * 0.4)) + 0.5;\n\n  vec2 uvC = uv;\n  vec2 uvN = uv;\n  float m = smoothstep(0.0, 1.0, p);\n\n  if (uReduce < 0.5) {\n    if (uMode == 3) {\n      vec2 c = uv - 0.5;\n      float r = length(c);\n      float ang = env * uIntensity * 3.5 * (1.0 - r);\n      uvC = rot(ang) * c + 0.5;\n      uvN = rot(-ang) * c + 0.5;\n      m = smoothstep(0.0, 1.0, p);\n    } else if (uMode == 1) {\n      float d = distance(uv, uPointer);\n      float ring = p * 1.6;\n      float wave = sin((d - ring) * 30.0) * env;\n      vec2 dir = normalize(uv - uPointer + 1e-4);\n      vec2 disp = dir * wave * uIntensity * 0.25;\n      uvC = uv + disp;\n      uvN = uv + disp * 0.6;\n      m = 1.0 - smoothstep(ring - 0.03, ring + 0.03, d);\n    } else if (uMode == 2) {\n      float slices = 14.0;\n      float row = floor(uv.y * slices);\n      float rnd = hash11(row);\n      vec2 disp = vec2((rnd - 0.5) * env * uIntensity * 0.6, 0.0);\n      uvC = uv + disp;\n      uvN = uv + disp;\n      float localX = uDir > 0.0 ? uv.x : 1.0 - uv.x;\n      float th = p * 1.5 - 0.25 + (rnd - 0.5) * 0.25;\n      m = 1.0 - smoothstep(th - 0.06, th + 0.06, localX);\n    } else {\n      float nn = fbm(uv * uScale + uTime * 0.03);\n      float warp = fbm(uv * uScale * 1.7 - uTime * 0.02);\n      vec2 g = vec2(nn, warp) - 0.5;\n      uvC = uv + g * uIntensity * 0.5 * p;\n      uvN = uv - g * uIntensity * 0.5 * (1.0 - p);\n      m = smoothstep(nn - 0.15, nn + 0.15, p);\n    }\n  }\n\n  vec2 sC = coverUV(uvC, uResolution, uCurrentSize);\n  vec2 sN = coverUV(uvN, uResolution, uNextSize);\n\n  float ca = uReduce < 0.5 ? uAberration * env * 0.03 : 0.0;\n\n  vec3 colC = vec3(\n    texture2D(tCurrent, sC + vec2(ca, 0.0)).r,\n    texture2D(tCurrent, sC).g,\n    texture2D(tCurrent, sC - vec2(ca, 0.0)).b\n  );\n  vec3 colN = vec3(\n    texture2D(tNext, sN + vec2(ca, 0.0)).r,\n    texture2D(tNext, sN).g,\n    texture2D(tNext, sN - vec2(ca, 0.0)).b\n  );\n\n  vec3 col = mix(colC, colN, m);\n\n  float vig = smoothstep(1.25, 0.25, length(uv - 0.5));\n  col = mix(col, uOverlay, (1.0 - vig) * 0.28);\n\n  gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nfunction makeFallbackTexture(gl) {\n  const size = 4;\n  const data = new Uint8Array(size * size * 4);\n  for (let i = 0; i < size * size; i++) {\n    data[i * 4] = 24;\n    data[i * 4 + 1] = 24;\n    data[i * 4 + 2] = 28;\n    data[i * 4 + 3] = 255;\n  }\n  return new Texture(gl, { image: data, width: size, height: size, generateMipmaps: false });\n}\n\nfunction hexToRgb(hex) {\n  let h = (hex || '#000000').replace('#', '');\n  if (h.length === 3) {\n    h = h\n      .split('')\n      .map(c => c + c)\n      .join('');\n  }\n  const n = parseInt(h, 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nclass MorphEngine {\n  constructor(container, { items, startIndex, reducedMotion, getOptions, onIndexChange, dprCap }) {\n    this.container = container;\n    this.items = items;\n    this.getOptions = getOptions;\n    this.onIndexChange = onIndexChange;\n    this.reducedMotion = reducedMotion;\n\n    this.current = startIndex;\n    this.animating = false;\n    this.dragging = false;\n    this.dragDir = 0;\n    this.shownIndex = startIndex;\n    this.tween = null;\n\n    this.renderer = new Renderer({\n      alpha: false,\n      antialias: true,\n      dpr: Math.min(window.devicePixelRatio || 1, dprCap)\n    });\n    this.gl = this.renderer.gl;\n    this.gl.clearColor(0.05, 0.05, 0.06, 1);\n\n    this.canvas = this.gl.canvas;\n    this.canvas.className = 'morph-slider-canvas';\n    container.appendChild(this.canvas);\n\n    this.geometry = new Triangle(this.gl);\n\n    this.textures = this.items.map(() => makeFallbackTexture(this.gl));\n    this.sizes = this.items.map(() => [1, 1]);\n\n    const opts = this.getOptions();\n    this.program = new Program(this.gl, {\n      vertex: vertexShader,\n      fragment: fragmentShader,\n      uniforms: {\n        tCurrent: { value: this.textures[this.current] },\n        tNext: { value: this.textures[this.current] },\n        uResolution: { value: [1, 1] },\n        uCurrentSize: { value: this.sizes[this.current] },\n        uNextSize: { value: this.sizes[this.current] },\n        uProgress: { value: 0 },\n        uDir: { value: 1 },\n        uMode: { value: TRANSITIONS[opts.transition] ?? 0 },\n        uIntensity: { value: opts.intensity },\n        uScale: { value: opts.scale },\n        uAberration: { value: opts.aberration },\n        uDrift: { value: opts.drift },\n        uTime: { value: 0 },\n        uReduce: { value: reducedMotion ? 1 : 0 },\n        uPointer: { value: [0.5, 0.5] },\n        uOverlay: { value: hexToRgb(opts.overlayColor) }\n      }\n    });\n\n    this.mesh = new Mesh(this.gl, { geometry: this.geometry, program: this.program });\n\n    this.boundContextLost = this.onContextLost.bind(this);\n    this.canvas.addEventListener('webglcontextlost', this.boundContextLost, false);\n\n    this.resizeObserver = new ResizeObserver(() => this.resize());\n    this.resizeObserver.observe(container);\n    this.resize();\n\n    this.loadTextures();\n\n    this.boundLoop = this.loop.bind(this);\n    this.raf = requestAnimationFrame(this.boundLoop);\n  }\n\n  loadTextures() {\n    this.items.forEach((item, index) => {\n      const img = new Image();\n      img.crossOrigin = 'anonymous';\n      img.src = item.image;\n      img.onload = () => {\n        const texture = new Texture(this.gl, { generateMipmaps: false });\n        texture.image = img;\n        this.textures[index] = texture;\n        this.sizes[index] = [img.naturalWidth || 1, img.naturalHeight || 1];\n        if (index === this.current) {\n          this.program.uniforms.tCurrent.value = texture;\n          this.program.uniforms.uCurrentSize.value = this.sizes[index];\n        }\n      };\n      img.onerror = () => {};\n    });\n  }\n\n  resize() {\n    const rect = this.container.getBoundingClientRect();\n    const w = Math.max(rect.width, 1);\n    const h = Math.max(rect.height, 1);\n    this.renderer.setSize(w, h);\n    this.program.uniforms.uResolution.value = [this.gl.canvas.width, this.gl.canvas.height];\n  }\n\n  syncOptions() {\n    const opts = this.getOptions();\n    this.program.uniforms.uMode.value = TRANSITIONS[opts.transition] ?? 0;\n    this.program.uniforms.uIntensity.value = opts.intensity;\n    this.program.uniforms.uScale.value = opts.scale;\n    this.program.uniforms.uAberration.value = opts.aberration;\n    this.program.uniforms.uDrift.value = opts.drift;\n    this.program.uniforms.uOverlay.value = hexToRgb(opts.overlayColor);\n  }\n\n  loop(t) {\n    this.program.uniforms.uTime.value = t * 0.001;\n    if (!this.dragging && !this.animating) this.syncOptions();\n    this.renderer.render({ scene: this.mesh });\n    this.raf = requestAnimationFrame(this.boundLoop);\n  }\n\n  wrap(i) {\n    const n = this.items.length;\n    return ((i % n) + n) % n;\n  }\n\n  prepareNext(dir) {\n    const target = this.wrap(this.current + dir);\n    this.program.uniforms.tCurrent.value = this.textures[this.current];\n    this.program.uniforms.uCurrentSize.value = this.sizes[this.current];\n    this.program.uniforms.tNext.value = this.textures[target];\n    this.program.uniforms.uNextSize.value = this.sizes[target];\n    this.program.uniforms.uDir.value = dir;\n    return target;\n  }\n\n  goTo(dir) {\n    if (this.animating || this.dragging || this.items.length < 2) return;\n    const opts = this.getOptions();\n    if (!opts.loop) {\n      const raw = this.current + dir;\n      if (raw < 0 || raw > this.items.length - 1) return;\n    }\n    this.syncOptions();\n    const target = this.prepareNext(dir);\n    this.animating = true;\n    this.announce(target);\n    const duration = this.reducedMotion ? Math.min(opts.duration, 0.4) : opts.duration;\n    this.tween = gsap.fromTo(\n      this.program.uniforms.uProgress,\n      { value: 0 },\n      {\n        value: 1,\n        duration,\n        ease: opts.ease,\n        onComplete: () => this.commit(target)\n      }\n    );\n  }\n\n  announce(index) {\n    if (index === this.shownIndex) return;\n    this.shownIndex = index;\n    if (this.onIndexChange) this.onIndexChange(index);\n  }\n\n  commit(target) {\n    this.current = target;\n    this.program.uniforms.tCurrent.value = this.textures[target];\n    this.program.uniforms.uCurrentSize.value = this.sizes[target];\n    this.program.uniforms.uProgress.value = 0;\n    this.animating = false;\n    this.tween = null;\n    this.announce(target);\n  }\n\n  next() {\n    this.goTo(1);\n  }\n\n  prev() {\n    this.goTo(-1);\n  }\n\n  setPointer(x, y) {\n    this.program.uniforms.uPointer.value = [x, y];\n  }\n\n  beginDrag() {\n    if (this.animating || this.items.length < 2) return false;\n    this.dragging = true;\n    this.dragDir = 0;\n    this.syncOptions();\n    return true;\n  }\n\n  drag(ndx) {\n    if (!this.dragging) return;\n    const opts = this.getOptions();\n    const dir = ndx < 0 ? 1 : -1;\n    if (!opts.loop) {\n      const raw = this.current + dir;\n      if (raw < 0 || raw > this.items.length - 1) {\n        this.program.uniforms.uProgress.value = 0;\n        return;\n      }\n    }\n    if (dir !== this.dragDir) {\n      this.dragDir = dir;\n      this.prepareNext(dir);\n    }\n    const progress = Math.min(Math.abs(ndx), 1);\n    this.program.uniforms.uProgress.value = progress;\n    this.announce(progress > 0.5 ? this.wrap(this.current + dir) : this.current);\n  }\n\n  endDrag() {\n    if (!this.dragging) return;\n    this.dragging = false;\n    const p = this.program.uniforms.uProgress.value;\n    if (this.dragDir === 0) return;\n    const target = this.wrap(this.current + this.dragDir);\n    const duration = this.reducedMotion ? 0.3 : 0.5;\n    this.animating = true;\n    if (p > 0.4) {\n      this.announce(target);\n      this.tween = gsap.to(this.program.uniforms.uProgress, {\n        value: 1,\n        duration,\n        ease: 'power2.out',\n        onComplete: () => this.commit(target)\n      });\n    } else {\n      this.announce(this.current);\n      this.tween = gsap.to(this.program.uniforms.uProgress, {\n        value: 0,\n        duration,\n        ease: 'power2.out',\n        onComplete: () => {\n          this.animating = false;\n          this.tween = null;\n        }\n      });\n    }\n  }\n\n  onContextLost(e) {\n    e.preventDefault();\n    cancelAnimationFrame(this.raf);\n  }\n\n  destroy() {\n    cancelAnimationFrame(this.raf);\n    if (this.tween) this.tween.kill();\n    this.resizeObserver.disconnect();\n    this.canvas.removeEventListener('webglcontextlost', this.boundContextLost);\n    this.textures.forEach(tex => {\n      if (tex && tex.texture) this.gl.deleteTexture(tex.texture);\n    });\n    if (this.program && this.program.program) this.gl.deleteProgram(this.program.program);\n    const ext = this.gl.getExtension('WEBGL_lose_context');\n    if (ext) ext.loseContext();\n    if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas);\n  }\n}\n\nexport default function MorphSlider({\n  items = DEFAULT_ITEMS,\n  startIndex = 0,\n  transition = 'melt',\n  duration = 1.1,\n  ease = 'power2.inOut',\n  intensity = 0.55,\n  scale = 2.4,\n  aberration = 0.35,\n  drift = 0.4,\n  autoplay = false,\n  autoplayDelay = 4,\n  loop = true,\n  radius = 16,\n  overlayColor = '#000000',\n  showCaptions = true,\n  showControls = true,\n  showIndicators = true,\n  className = '',\n  ...props\n}) {\n  const containerRef = useRef(null);\n  const engineRef = useRef(null);\n  const [index, setIndex] = useState(startIndex);\n  const [hovering, setHovering] = useState(false);\n\n  const optsRef = useRef();\n  optsRef.current = { transition, duration, ease, intensity, scale, aberration, drift, overlayColor, loop };\n\n  useEffect(() => {\n    if (!containerRef.current) return undefined;\n    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    const engine = new MorphEngine(containerRef.current, {\n      items,\n      startIndex,\n      reducedMotion,\n      dprCap: 2,\n      getOptions: () => optsRef.current,\n      onIndexChange: setIndex\n    });\n    engineRef.current = engine;\n    setIndex(startIndex);\n\n    return () => {\n      engine.destroy();\n      engineRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [items, startIndex]);\n\n  const handleNext = useCallback(() => engineRef.current?.next(), []);\n  const handlePrev = useCallback(() => engineRef.current?.prev(), []);\n\n  useEffect(() => {\n    if (!autoplay || hovering) return undefined;\n    const id = setTimeout(() => engineRef.current?.next(), Math.max(autoplayDelay, 1) * 1000);\n    return () => clearTimeout(id);\n  }, [autoplay, autoplayDelay, hovering, index]);\n\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return undefined;\n    let startX = 0;\n    let width = 1;\n    let active = false;\n\n    const onDown = e => {\n      const rect = el.getBoundingClientRect();\n      width = rect.width || 1;\n      startX = e.clientX;\n      const px = (e.clientX - rect.left) / rect.width;\n      const py = (e.clientY - rect.top) / rect.height;\n      engineRef.current?.setPointer(px, 1 - py);\n      active = engineRef.current?.beginDrag() ?? false;\n      if (active && el.setPointerCapture) {\n        try {\n          el.setPointerCapture(e.pointerId);\n        } catch {}\n      }\n    };\n    const onMove = e => {\n      if (!active) return;\n      const ndx = (e.clientX - startX) / width;\n      engineRef.current?.drag(ndx);\n    };\n    const onUp = () => {\n      if (!active) return;\n      active = false;\n      engineRef.current?.endDrag();\n    };\n\n    el.addEventListener('pointerdown', onDown);\n    el.addEventListener('pointermove', onMove);\n    el.addEventListener('pointerup', onUp);\n    el.addEventListener('pointercancel', onUp);\n\n    return () => {\n      el.removeEventListener('pointerdown', onDown);\n      el.removeEventListener('pointermove', onMove);\n      el.removeEventListener('pointerup', onUp);\n      el.removeEventListener('pointercancel', onUp);\n    };\n  }, []);\n\n  const onKeyDown = useCallback(\n    e => {\n      if (e.key === 'ArrowRight') {\n        e.preventDefault();\n        handleNext();\n      } else if (e.key === 'ArrowLeft') {\n        e.preventDefault();\n        handlePrev();\n      }\n    },\n    [handleNext, handlePrev]\n  );\n\n  const hasCaptions = items.some(item => item.caption);\n\n  return (\n    <div\n      className={`morph-slider ${className}`.trim()}\n      style={{\n        borderRadius: `${radius}px`,\n        '--ms-swap': `${(duration * 0.66).toFixed(3)}s`,\n        '--ms-dot': `${(duration * 0.45).toFixed(3)}s`\n      }}\n      onMouseEnter={() => setHovering(true)}\n      onMouseLeave={() => setHovering(false)}\n      {...props}\n    >\n      <div\n        ref={containerRef}\n        className=\"morph-slider-stage\"\n        role=\"group\"\n        aria-roledescription=\"carousel\"\n        aria-label=\"Image morph slider\"\n        tabIndex={0}\n        onKeyDown={onKeyDown}\n      />\n\n      {showCaptions && hasCaptions && (\n        <div className=\"morph-slider-caption\" aria-live=\"polite\">\n          {items.map((item, i) =>\n            item.caption ? (\n              <span\n                key={i}\n                aria-hidden={i === index ? undefined : true}\n                className={`morph-slider-caption-text ${i === index ? 'is-active' : ''}`}\n              >\n                {item.caption}\n              </span>\n            ) : null\n          )}\n        </div>\n      )}\n\n      {showControls && (\n        <div className=\"morph-slider-controls\">\n          <button type=\"button\" className=\"morph-slider-btn\" aria-label=\"Previous slide\" onClick={handlePrev}>\n            <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n              <path\n                d=\"M15 5l-7 7 7 7\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            </svg>\n          </button>\n          <button type=\"button\" className=\"morph-slider-btn\" aria-label=\"Next slide\" onClick={handleNext}>\n            <svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\">\n              <path\n                d=\"M9 5l7 7-7 7\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            </svg>\n          </button>\n        </div>\n      )}\n\n      {showIndicators && (\n        <div className=\"morph-slider-indicators\" role=\"tablist\" aria-label=\"Slides\">\n          {items.map((item, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              role=\"tab\"\n              aria-selected={i === index}\n              aria-label={`Go to slide ${i + 1}`}\n              className={`morph-slider-dot ${i === index ? 'is-active' : ''}`}\n              onClick={() => {\n                const engine = engineRef.current;\n                if (!engine || i === index) return;\n                engine.goTo(i > index ? 1 : -1);\n              }}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"ogl@^1.0.11",
		"gsap@^3.13.0"
	]
}