{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "warp-slider-html",
  "title": "Warp Slider",
  "description": "A smooth, infinite slider with dynamic warp distortion and momentum based scrolling built with HTML, CSS, JavaScript and Three.js.",
  "dependencies": [
    "three"
  ],
  "files": [
    {
      "path": "registry/html/atlasui/warp-slider/index.html",
      "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"stylesheet\" href=\"style.css\" />\n    <title>Warp Slider - Atlas UI</title>\n  </head>\n  <body>\n    <section class=\"slider\">\n      <div class=\"slide-info\">\n        <p id=\"slide-title\"></p>\n        <p id=\"slide-count\"></p>\n      </div>\n    </section>\n\n    <script type=\"module\" src=\"script.js\"></script>\n  </body>\n</html>\n",
      "type": "registry:file",
      "target": "~/warp-slider/index.html"
    },
    {
      "path": "registry/html/atlasui/warp-slider/style.css",
      "content": ":root {\n  --background: light-dark(\n    oklch(100% 0.00011 271.152),\n    oklch(18.22% 0.00002 271.152)\n  );\n  --foreground: light-dark(\n    oklch(18.22% 0.00002 271.152),\n    oklch(98.511% 0.00011 271.152)\n  );\n}\n\nhtml {\n  color-scheme: light dark;\n}\n\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n  user-select: none;\n}\n\nbody {\n  font-family: \"Inter\", sans-serif;\n  background-color: var(--background);\n  color: var(--foreground);\n}\n\nsection {\n  position: relative;\n  width: 100%;\n  height: 100svh;\n  overflow: hidden;\n}\n\np {\n  font-weight: 500;\n  font-size: 1.25rem;\n}\n\ncanvas {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.slide-info {\n  position: absolute;\n  top: 50%;\n  left: 0;\n  transform: translateY(-50%);\n  width: 100%;\n  padding: 0 2rem;\n  display: flex;\n  justify-content: space-between;\n  z-index: 2;\n}\n",
      "type": "registry:file",
      "target": "~/warp-slider/style.css"
    },
    {
      "path": "registry/html/atlasui/warp-slider/script.js",
      "content": "import * as THREE from \"three\";\n\nconst slides = [\n  {\n    name: \"Orion\",\n    img: \"https://images.unsplash.com/photo-1773311583232-0a5765cf08ac?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Aries\",\n    img: \"https://images.unsplash.com/photo-1773530615360-eb180ed88622?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Taurus\",\n    img: \"https://images.unsplash.com/photo-1773011389556-2bf0474cefd9?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Gemini\",\n    img: \"https://images.unsplash.com/photo-1771030668418-390b3edf33e4?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Cancer\",\n    img: \"https://images.unsplash.com/photo-1772289935218-78954128824a?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Leo\",\n    img: \"https://images.unsplash.com/photo-1770988966858-da0a4bde2f0b?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Virgo\",\n    img: \"https://images.unsplash.com/photo-1771308457683-eef03842c1ed?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Libra\",\n    img: \"https://images.unsplash.com/photo-1770341989953-f3efb336f7eb?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Scorpio\",\n    img: \"https://images.unsplash.com/photo-1595660981167-fe72e31f6a40?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n  {\n    name: \"Sagittarius\",\n    img: \"https://images.unsplash.com/photo-1709570597799-7d94d430a2bc?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n  },\n];\n\nconst 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\nconst sliderSection = document.querySelector(\"section.slider\");\n\nconst canvas = document.createElement(\"canvas\");\nsliderSection?.appendChild(canvas);\n\nconst titleElement = document.querySelector(\"p#slide-title\");\nconst counterElement = document.querySelector(\"p#slide-count\");\n\nconst renderer = new THREE.WebGLRenderer({\n  canvas,\n  antialias: true,\n  preserveDrawingBuffer: true,\n});\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n\nconst getBackgroundColor = () => {\n  const htmlTheme = document.documentElement.getAttribute(\"data-theme\");\n  const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n  const isDark = htmlTheme === \"dark\" || (!htmlTheme && prefersDark);\n\n  return isDark ? \"#111111\" : \"#ffffff\";\n};\n\nconst scene = new THREE.Scene();\nscene.background = new THREE.Color(getBackgroundColor());\n\nconst updateSceneBackground = () => {\n  scene.background = new THREE.Color(getBackgroundColor());\n};\n\nconst darkModeMediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\ndarkModeMediaQuery.addEventListener(\"change\", updateSceneBackground);\n\nconst themeObserver = new MutationObserver(updateSceneBackground);\nthemeObserver.observe(document.documentElement, {\n  attributes: true,\n  attributeFilter: [\"data-theme\", \"style\"],\n});\n\nconst camera = new THREE.PerspectiveCamera(\n  45,\n  window.innerWidth / window.innerHeight,\n  0.1,\n  100,\n);\ncamera.position.z = 5;\n\nconst wrap = (value, range) => ((value % range) + range) % range;\n\nconst zeroPad = (n) => String(n).padStart(2, \"0\");\n\nconst totalSlides = slides.length;\n\nif (![\"vertical\", \"horizontal\"].includes(config.orientation)) {\n  console.error(\n    `Invalid orientation \"${config.orientation}\". Accepts only: vertical | horizontal. Defaulting to \"vertical\".`,\n  );\n  config.orientation = \"vertical\";\n}\n\nconst isHorizontal = config.orientation === \"horizontal\";\nconst directionSign = 1;\n\nconst slideHeights = Array.from(\n  { length: totalSlides },\n  () =>\n    config.minHeight + Math.random() * (config.maxHeight - config.minHeight),\n);\n\nconst slideSizes = isHorizontal\n  ? slideHeights.map((h) => h * config.aspectRatio)\n  : slideHeights;\n\nconst slideOffsets = [];\nlet stackPosition = 0;\n\nfor (let i = 0; i < totalSlides; i++) {\n  if (i === 0) {\n    slideOffsets.push(0);\n    stackPosition = slideSizes[0] / 2;\n  } else {\n    stackPosition += config.gap + slideSizes[i] / 2;\n    slideOffsets.push(stackPosition);\n    stackPosition += slideSizes[i] / 2;\n  }\n}\n\nconst loopLength = stackPosition + config.gap + slideSizes[0] / 2;\nconst halfLoop = loopLength / 2;\n\nconst meshes = [];\nconst textureLoader = new THREE.TextureLoader();\n\nfor (let i = 0; i < totalSlides; i++) {\n  const height = slideHeights[i];\n  const width = height * config.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);\n\n  mesh.userData = {\n    originalVertices: [...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\nfunction applyDistortion(mesh, positionOnAxis, strength) {\n  const positions = mesh.geometry.attributes.position;\n  const original = mesh.userData.originalVertices;\n  const falloffRadius = isHorizontal ? 3.5 : 2;\n\n  for (let i = 0; i < positions.count; i++) {\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\n    const falloff = Math.max(0, 1 - distance / falloffRadius);\n\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\nlet scrollPosition = 0;\nlet scrollTarget = 0;\nlet scrollMomentum = 0;\nlet isScrolling = false;\nlet lastFrameTime = 0;\n\nlet distortionAmount = 0;\nlet distortionTarget = 0;\nlet velocityPeak = 0;\nlet scrollDirection = 0;\nlet directionTarget = 0;\nconst velocityHistory = [0, 0, 0, 0, 0];\n\nlet isDragging = false;\nlet dragStart = 0;\nlet dragDelta = 0;\nlet touchStart = 0;\nlet touchLast = 0;\n\nlet activeSlideIndex = -1;\n\nconst addDistortionBurst = (amount) => {\n  distortionTarget = Math.min(1, distortionTarget + amount);\n};\n\nwindow.addEventListener(\n  \"wheel\",\n  (e) => {\n    e.preventDefault();\n\n    const rawDelta = isHorizontal ? e.deltaX || e.deltaY : e.deltaY;\n    const clampedDelta =\n      Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), config.wheelMax);\n\n    addDistortionBurst(Math.abs(clampedDelta) * 0.001);\n    scrollTarget += clampedDelta * config.wheelSpeed * directionSign;\n    isScrolling = true;\n\n    clearTimeout(window._scrollTimeout);\n    window._scrollTimeout = setTimeout(() => (isScrolling = false), 150);\n  },\n  { passive: false },\n);\n\nwindow.addEventListener(\n  \"touchstart\",\n  (e) => {\n    const pos = isHorizontal ? e.touches[0].clientX : e.touches[0].clientY;\n    touchStart = touchLast = pos;\n    isScrolling = true;\n    scrollMomentum = 0;\n  },\n  { passive: false },\n);\n\nwindow.addEventListener(\n  \"touchmove\",\n  (e) => {\n    e.preventDefault();\n\n    const current = isHorizontal ? e.touches[0].clientX : e.touches[0].clientY;\n    const delta = current - touchLast;\n    touchLast = current;\n\n    addDistortionBurst(Math.abs(delta) * 0.02);\n    scrollTarget -= delta * config.touchSpeed * directionSign;\n    isScrolling = true;\n  },\n  { passive: false },\n);\n\nwindow.addEventListener(\"touchend\", () => {\n  const swipeVelocity = (touchLast - touchStart) * 0.005;\n\n  if (Math.abs(swipeVelocity) > 0.5) {\n    scrollMomentum = -swipeVelocity * config.touchMomentum * directionSign;\n    addDistortionBurst(Math.abs(swipeVelocity) * 0.45);\n    isScrolling = true;\n    setTimeout(() => (isScrolling = false), 800);\n  }\n});\n\ncanvas.style.cursor = \"grab\";\n\nwindow.addEventListener(\"mousedown\", (e) => {\n  isDragging = true;\n  dragStart = isHorizontal ? e.clientX : e.clientY;\n  dragDelta = 0;\n  scrollMomentum = 0;\n  canvas.style.cursor = \"grabbing\";\n});\n\nwindow.addEventListener(\"mousemove\", (e) => {\n  if (!isDragging) return;\n\n  const current = isHorizontal ? e.clientX : e.clientY;\n  const delta = current - dragStart;\n  dragStart = current;\n  dragDelta = delta;\n\n  addDistortionBurst(Math.abs(delta) * 0.02);\n  scrollTarget -= delta * config.dragSpeed * directionSign;\n  isScrolling = true;\n});\n\nwindow.addEventListener(\"mouseup\", () => {\n  if (!isDragging) return;\n\n  isDragging = false;\n  canvas.style.cursor = \"grab\";\n\n  if (Math.abs(dragDelta) > 2) {\n    scrollMomentum = -dragDelta * config.dragMomentum * directionSign;\n    addDistortionBurst(Math.abs(dragDelta) * 0.005);\n    isScrolling = true;\n    setTimeout(() => (isScrolling = false), 800);\n  }\n});\n\nwindow.addEventListener(\"resize\", () => {\n  camera.aspect = window.innerWidth / window.innerHeight;\n  camera.updateProjectionMatrix();\n  renderer.setSize(window.innerWidth, window.innerHeight);\n});\n\nfunction animate(time) {\n  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 *= config.momentumFriction;\n\n    if (Math.abs(scrollMomentum) < config.momentumThreshold) {\n      scrollMomentum = 0;\n    }\n  }\n\n  scrollPosition += (scrollTarget - scrollPosition) * config.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((a, b) => a + b) / 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) * config.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 + config.maxHeight) {\n      applyDistortion(mesh, pos, config.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\nanimate();\n",
      "type": "registry:file",
      "target": "~/warp-slider/script.js"
    }
  ],
  "meta": {
    "framework": "html"
  },
  "type": "registry:file"
}