{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "warp-slider",
  "title": "Warp Slider",
  "description": "A smooth, infinite slider with dynamic warp distortion and momentum based scrolling.",
  "dependencies": [
    "three"
  ],
  "devDependencies": [
    "@types/three"
  ],
  "files": [
    {
      "path": "registry/react/atlasui/warp-slider.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\n\nimport * as THREE from \"three\";\n\ninterface WarpSliderProps {\n  slides: {\n    name: string;\n    img: string;\n  }[];\n  config?: Partial<{\n    orientation: \"vertical\" | \"horizontal\";\n    minHeight: number;\n    maxHeight: number;\n    aspectRatio: number;\n    gap: number;\n    smoothing: number;\n    distortionStrength: number;\n    distortionSmoothing: number;\n    momentumFriction: number;\n    momentumThreshold: number;\n    wheelSpeed: number;\n    wheelMax: number;\n    dragSpeed: number;\n    dragMomentum: number;\n    touchSpeed: number;\n    touchMomentum: number;\n  }>;\n}\n\nconst labToHex = (css: string): string => {\n  const values = css\n    .replace(/lab|\\(|\\)/g, \"\")\n    .split(/[ ,/]+/)\n    .filter(Boolean);\n\n  const [L, a, b] = values;\n\n  const l = parseFloat(L.replace(\"%\", \"\")) / 100;\n  const A = parseFloat(a);\n  const B = parseFloat(b);\n\n  // LAB → XYZ\n  const y = (l + 0.16) / 1.16;\n  const x = A / 5 + y;\n  const z = y - B / 2;\n\n  const x3 = x ** 3;\n  const y3 = y ** 3;\n  const z3 = z ** 3;\n\n  const X = 0.95047 * (x3 > 0.008856 ? x3 : (x - 16 / 116) / 7.787);\n  const Y = 1.0 * (y3 > 0.008856 ? y3 : (y - 16 / 116) / 7.787);\n  const Z = 1.08883 * (z3 > 0.008856 ? z3 : (z - 16 / 116) / 7.787);\n\n  // XYZ → linear RGB\n  let r = X * 3.2406 + Y * -1.5372 + Z * -0.4986;\n  let g = X * -0.9689 + Y * 1.8758 + Z * 0.0415;\n  let b_ = X * 0.0557 + Y * -0.204 + Z * 1.057;\n\n  // linear → sRGB\n  const toSRGB = (x: number) =>\n    x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;\n\n  r = toSRGB(r);\n  g = toSRGB(g);\n  b_ = toSRGB(b_);\n\n  // clamp\n  r = Math.min(Math.max(r, 0), 1);\n  g = Math.min(Math.max(g, 0), 1);\n  b_ = Math.min(Math.max(b_, 0), 1);\n\n  const toHex = (x: number) =>\n    Math.round(x * 255)\n      .toString(16)\n      .padStart(2, \"0\");\n\n  return `#${toHex(r)}${toHex(g)}${toHex(b_)}`;\n};\n\nexport const WarpSlider = ({\n  slides,\n  config: {\n    orientation = \"vertical\",\n    minHeight = 1,\n    maxHeight = 1.5,\n    aspectRatio = 1.5,\n    gap = 0.05,\n    smoothing = 0.05,\n    distortionStrength = 2.5,\n    distortionSmoothing = 0.1,\n    momentumFriction = 0.95,\n    momentumThreshold = 0.001,\n    wheelSpeed = 0.01,\n    wheelMax = 150,\n    dragSpeed = 0.01,\n    dragMomentum = 0.01,\n    touchSpeed = 0.01,\n    touchMomentum = 0.1,\n  } = {},\n}: WarpSliderProps) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null);\n  const titleRef = useRef<HTMLParagraphElement | null>(null);\n  const counterRef = useRef<HTMLParagraphElement | null>(null);\n\n  const totalSlides = slides.length;\n\n  const defaultCount = useMemo(() => {\n    if (!totalSlides) {\n      return \"00/00\";\n    }\n    const zeroPad = (n: number) => String(n).padStart(2, \"0\");\n    return `${zeroPad(1)}/${zeroPad(totalSlides)}`;\n  }, [totalSlides]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const titleElement = titleRef.current;\n    const counterElement = counterRef.current;\n\n    if (!canvas || !titleElement || !counterElement || slides.length === 0) {\n      return;\n    }\n\n    const renderer = new THREE.WebGLRenderer({\n      canvas,\n      antialias: true,\n      preserveDrawingBuffer: true,\n    });\n    renderer.setSize(window.innerWidth, window.innerHeight);\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n\n    const getBackgroundColor = () => {\n      const bgCssColor = getComputedStyle(document.body)\n        .getPropertyValue(\"--color-background\")\n        .trim();\n      return labToHex(bgCssColor);\n    };\n\n    const scene = new THREE.Scene();\n    scene.background = new THREE.Color(getBackgroundColor());\n\n    const themeObserver = new MutationObserver(() => {\n      scene.background = new THREE.Color(getBackgroundColor());\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    });\n\n    const camera = new THREE.PerspectiveCamera(\n      45,\n      window.innerWidth / window.innerHeight,\n      0.1,\n      100\n    );\n    camera.position.z = 5;\n\n    const isHorizontal = orientation === \"horizontal\";\n\n    const wrap = (value: number, range: number) =>\n      ((value % range) + range) % range;\n    const zeroPad = (n: number) => String(n).padStart(2, \"0\");\n\n    const slideHeights = Array.from(\n      { length: totalSlides },\n      () => minHeight + Math.random() * (maxHeight - minHeight)\n    );\n\n    const slideSizes = isHorizontal\n      ? slideHeights.map((h) => h * aspectRatio)\n      : slideHeights;\n\n    const slideOffsets: number[] = [];\n    let stackPosition = 0;\n\n    for (let i = 0; i < totalSlides; i += 1) {\n      if (i === 0) {\n        slideOffsets.push(0);\n        stackPosition = slideSizes[0] / 2;\n      } else {\n        stackPosition += gap + slideSizes[i] / 2;\n        slideOffsets.push(stackPosition);\n        stackPosition += slideSizes[i] / 2;\n      }\n    }\n\n    const loopLength = stackPosition + gap + slideSizes[0] / 2;\n    const halfLoop = loopLength / 2;\n\n    const directionSign = 1;\n\n    type MeshWithData = THREE.Mesh<\n      THREE.PlaneGeometry,\n      THREE.MeshBasicMaterial\n    > & {\n      userData: {\n        originalVertices: number[];\n        offset: number;\n        name: string;\n        index: number;\n      };\n    };\n\n    const meshes: MeshWithData[] = [];\n    const textureLoader = new THREE.TextureLoader();\n\n    for (let i = 0; i < totalSlides; i += 1) {\n      const height = slideHeights[i];\n      const width = height * aspectRatio;\n\n      const geometry = new THREE.PlaneGeometry(width, height, 32, 16);\n      const material = new THREE.MeshBasicMaterial({\n        side: THREE.DoubleSide,\n        color: 0x999999,\n      });\n      const mesh = new THREE.Mesh(geometry, material) as MeshWithData;\n\n      mesh.userData = {\n        originalVertices: Array.from(geometry.attributes.position.array),\n        offset: slideOffsets[i],\n        name: slides[i].name,\n        index: i,\n      };\n\n      textureLoader.load(slides[i].img, (texture) => {\n        texture.colorSpace = THREE.SRGBColorSpace;\n        material.map = texture;\n        material.color.set(0xffffff);\n        material.needsUpdate = true;\n\n        const imageAspect = texture.image.width / texture.image.height;\n        const planeAspect = width / height;\n        const ratio = imageAspect / planeAspect;\n\n        if (ratio > 1) {\n          mesh.scale.y = 1 / ratio;\n        } else {\n          mesh.scale.x = ratio;\n        }\n      });\n\n      scene.add(mesh);\n      meshes.push(mesh);\n    }\n\n    const applyDistortion = (\n      mesh: MeshWithData,\n      positionOnAxis: number,\n      strength: number\n    ) => {\n      const positions = mesh.geometry.attributes.position;\n      const original = mesh.userData.originalVertices;\n\n      for (let i = 0; i < positions.count; i += 1) {\n        const x = original[i * 3];\n        const y = original[i * 3 + 1];\n\n        const distance = isHorizontal\n          ? Math.sqrt((positionOnAxis + x) ** 2 + y * y)\n          : Math.sqrt(x * x + (positionOnAxis + y) ** 2);\n        const falloffRadius = isHorizontal ? 3.5 : 2;\n        const falloff = Math.max(0, 1 - distance / falloffRadius);\n        const bend = Math.pow(Math.sin((falloff * Math.PI) / 2), 1.5);\n        positions.setZ(i, bend * strength);\n      }\n\n      positions.needsUpdate = true;\n      mesh.geometry.computeVertexNormals();\n    };\n\n    let scrollPosition = 0;\n    let scrollTarget = 0;\n    let scrollMomentum = 0;\n    let isScrolling = false;\n    let lastFrameTime = 0;\n\n    let distortionAmount = 0;\n    let distortionTarget = 0;\n    let velocityPeak = 0;\n    let scrollDirection = 0;\n    let directionTarget = 0;\n    const velocityHistory = [0, 0, 0, 0, 0];\n\n    let isDragging = false;\n    let dragStart = 0;\n    let dragDelta = 0;\n    let touchStart = 0;\n    let touchLast = 0;\n    let activeSlideIndex = -1;\n    let animationFrame = 0;\n    let scrollTimeout: ReturnType<typeof setTimeout> | null = null;\n\n    const addDistortionBurst = (amount: number) => {\n      distortionTarget = Math.min(1, distortionTarget + amount);\n    };\n\n    const handleWheel = (e: WheelEvent) => {\n      e.preventDefault();\n\n      const rawDelta = isHorizontal\n        ? e.deltaX !== 0\n          ? e.deltaX\n          : e.deltaY\n        : e.deltaY;\n      const clampedDelta =\n        Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), wheelMax);\n      addDistortionBurst(Math.abs(clampedDelta) * 0.001);\n      scrollTarget += clampedDelta * wheelSpeed * directionSign;\n      isScrolling = true;\n\n      if (scrollTimeout) {\n        clearTimeout(scrollTimeout);\n      }\n      scrollTimeout = setTimeout(() => {\n        isScrolling = false;\n      }, 150);\n    };\n\n    const handleTouchStart = (e: TouchEvent) => {\n      const coord = isHorizontal ? e.touches[0].clientX : e.touches[0].clientY;\n      touchStart = coord;\n      touchLast = coord;\n      isScrolling = true;\n      scrollMomentum = 0;\n    };\n\n    const handleTouchMove = (e: TouchEvent) => {\n      e.preventDefault();\n\n      const coord = isHorizontal ? e.touches[0].clientX : e.touches[0].clientY;\n      const delta = coord - touchLast;\n      touchLast = coord;\n\n      addDistortionBurst(Math.abs(delta) * 0.02);\n      scrollTarget -= delta * touchSpeed * directionSign;\n      isScrolling = true;\n    };\n\n    const handleTouchEnd = () => {\n      const swipeVelocity = (touchLast - touchStart) * 0.005;\n\n      if (Math.abs(swipeVelocity) > 0.5) {\n        scrollMomentum = -swipeVelocity * touchMomentum * directionSign;\n        addDistortionBurst(Math.abs(swipeVelocity) * 0.45);\n        isScrolling = true;\n        setTimeout(() => {\n          isScrolling = false;\n        }, 800);\n      }\n    };\n\n    const handleMouseDown = (e: MouseEvent) => {\n      isDragging = true;\n      dragStart = isHorizontal ? e.clientX : e.clientY;\n      dragDelta = 0;\n      scrollMomentum = 0;\n      canvas.style.cursor = \"grabbing\";\n    };\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (!isDragging) {\n        return;\n      }\n\n      const coord = isHorizontal ? e.clientX : e.clientY;\n      const delta = coord - dragStart;\n      dragStart = coord;\n      dragDelta = delta;\n\n      addDistortionBurst(Math.abs(delta) * 0.02);\n      scrollTarget -= delta * dragSpeed * directionSign;\n      isScrolling = true;\n    };\n\n    const handleMouseUp = () => {\n      if (!isDragging) {\n        return;\n      }\n\n      isDragging = false;\n      canvas.style.cursor = \"grab\";\n\n      if (Math.abs(dragDelta) > 2) {\n        scrollMomentum = -dragDelta * dragMomentum * directionSign;\n        addDistortionBurst(Math.abs(dragDelta) * 0.005);\n        isScrolling = true;\n        setTimeout(() => {\n          isScrolling = false;\n        }, 800);\n      }\n    };\n\n    const handleResize = () => {\n      camera.aspect = window.innerWidth / window.innerHeight;\n      camera.updateProjectionMatrix();\n      renderer.setSize(window.innerWidth, window.innerHeight);\n    };\n\n    canvas.style.cursor = \"grab\";\n\n    canvas.addEventListener(\"wheel\", handleWheel, { passive: false });\n    canvas.addEventListener(\"touchstart\", handleTouchStart, { passive: false });\n    canvas.addEventListener(\"touchmove\", handleTouchMove, { passive: false });\n    canvas.addEventListener(\"touchend\", handleTouchEnd);\n    canvas.addEventListener(\"mousedown\", handleMouseDown);\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    window.addEventListener(\"mouseup\", handleMouseUp);\n    window.addEventListener(\"resize\", handleResize);\n\n    const animate = (time: number) => {\n      animationFrame = window.requestAnimationFrame(animate);\n\n      const deltaTime = lastFrameTime ? (time - lastFrameTime) / 1000 : 0.016;\n      lastFrameTime = time;\n\n      const previousScroll = scrollPosition;\n\n      if (isScrolling) {\n        scrollTarget += scrollMomentum;\n        scrollMomentum *= momentumFriction;\n\n        if (Math.abs(scrollMomentum) < momentumThreshold) {\n          scrollMomentum = 0;\n        }\n      }\n\n      scrollPosition += (scrollTarget - scrollPosition) * smoothing;\n\n      const frameDelta = scrollPosition - previousScroll;\n\n      if (Math.abs(frameDelta) > 0.00001) {\n        directionTarget = frameDelta > 0 ? 1 : -1;\n      }\n      scrollDirection += (directionTarget - scrollDirection) * 0.08;\n\n      const velocity = Math.abs(frameDelta) / deltaTime;\n\n      velocityHistory.push(velocity);\n      velocityHistory.shift();\n\n      const averageVelocity =\n        velocityHistory.reduce((acc, current) => acc + current, 0) /\n        velocityHistory.length;\n\n      if (averageVelocity > velocityPeak) {\n        velocityPeak = averageVelocity;\n      }\n\n      const isDecelerating =\n        averageVelocity / (velocityPeak + 0.001) < 0.7 && velocityPeak > 0.5;\n\n      if (velocity > 0.05) {\n        distortionTarget = Math.max(\n          distortionTarget,\n          Math.min(1, velocityPeak * 0.1)\n        );\n      }\n\n      if (isDecelerating || averageVelocity < 0.2) {\n        distortionTarget *= isDecelerating ? 0.95 : 0.855;\n      }\n\n      distortionAmount +=\n        (distortionTarget - distortionAmount) * distortionSmoothing;\n\n      const signedDistortion = distortionAmount * scrollDirection;\n\n      let closestDistance = Infinity;\n      let closestIndex = 0;\n\n      meshes.forEach((mesh) => {\n        const { offset } = mesh.userData;\n\n        let pos = -(offset - wrap(scrollPosition, loopLength));\n        pos = wrap(pos + halfLoop, loopLength) - halfLoop;\n        if (isHorizontal) pos = -pos;\n\n        if (isHorizontal) {\n          mesh.position.x = pos;\n          mesh.position.y = 0;\n        } else {\n          mesh.position.y = pos;\n          mesh.position.x = 0;\n        }\n\n        if (Math.abs(pos) < closestDistance) {\n          closestDistance = Math.abs(pos);\n          closestIndex = mesh.userData.index;\n        }\n\n        if (Math.abs(pos) < halfLoop + maxHeight) {\n          applyDistortion(mesh, pos, distortionStrength * signedDistortion);\n        }\n      });\n\n      if (closestIndex !== activeSlideIndex) {\n        activeSlideIndex = closestIndex;\n        titleElement.textContent = slides[activeSlideIndex].name;\n        counterElement.textContent = `${zeroPad(activeSlideIndex + 1)}/${zeroPad(totalSlides)}`;\n      }\n\n      renderer.render(scene, camera);\n    };\n\n    animate(0);\n\n    return () => {\n      if (scrollTimeout) {\n        clearTimeout(scrollTimeout);\n      }\n\n      window.cancelAnimationFrame(animationFrame);\n      canvas.removeEventListener(\"wheel\", handleWheel);\n      canvas.removeEventListener(\"touchstart\", handleTouchStart);\n      canvas.removeEventListener(\"touchmove\", handleTouchMove);\n      canvas.removeEventListener(\"touchend\", handleTouchEnd);\n      canvas.removeEventListener(\"mousedown\", handleMouseDown);\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n      window.removeEventListener(\"mouseup\", handleMouseUp);\n      window.removeEventListener(\"resize\", handleResize);\n\n      meshes.forEach((mesh) => {\n        scene.remove(mesh);\n        mesh.geometry.dispose();\n        if (mesh.material.map) {\n          mesh.material.map.dispose();\n        }\n        mesh.material.dispose();\n      });\n\n      renderer.dispose();\n      themeObserver.disconnect();\n    };\n  }, [\n    aspectRatio,\n    dragMomentum,\n    dragSpeed,\n    distortionSmoothing,\n    distortionStrength,\n    gap,\n    maxHeight,\n    minHeight,\n    momentumFriction,\n    momentumThreshold,\n    orientation,\n    slides,\n    smoothing,\n    totalSlides,\n    touchMomentum,\n    touchSpeed,\n    wheelMax,\n    wheelSpeed,\n  ]);\n\n  return (\n    <section className=\"relative h-svh w-full overflow-hidden select-none\">\n      <div className=\"absolute top-1/2 left-0 z-2 flex w-full -translate-y-1/2 justify-between px-4\">\n        <p id=\"slide-title\" ref={titleRef} className=\"text-xl font-medium\">\n          {slides[0]?.name ?? \"\"}\n        </p>\n        <p id=\"slide-count\" ref={counterRef} className=\"text-xl font-medium\">\n          {defaultCount}\n        </p>\n      </div>\n\n      <canvas\n        ref={canvasRef}\n        className=\"absolute top-0 left-1/2 h-full w-full -translate-x-1/2 overflow-hidden\"\n      />\n    </section>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}