{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DepthCarousel-TS-CSS",
	"title": "DepthCarousel",
	"description": "Cards recede into depth on a 3D rail, with drag, keyboard and auto-advance.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "DepthCarousel.css",
			"target": "@components/DepthCarousel.css",
			"content": ".depth-carousel {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  min-height: 320px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  perspective: var(--dc-perspective, 1400px);\n  perspective-origin: 50% 50%;\n  touch-action: pan-y;\n  outline: none;\n  user-select: none;\n  -webkit-user-select: none;\n  cursor: grab;\n}\n\n.depth-carousel:active {\n  cursor: grabbing;\n}\n\n.depth-carousel:focus-visible {\n  outline: 2px solid rgba(255, 255, 255, 0.5);\n  outline-offset: 4px;\n  border-radius: 12px;\n}\n\n.depth-carousel__stage {\n  position: absolute;\n  inset: 0;\n  transform-style: preserve-3d;\n}\n\n.depth-carousel__card {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform-origin: center center;\n  overflow: hidden;\n  background: #0b0d12;\n  box-shadow:\n    0 30px 60px -20px rgba(0, 0, 0, 0.65),\n    0 8px 20px -10px rgba(0, 0, 0, 0.5);\n  will-change: transform, opacity, filter;\n  cursor: pointer;\n  transform: translate(-50%, -50%);\n}\n\n.depth-carousel__img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  display: block;\n  pointer-events: none;\n  -webkit-user-drag: none;\n}\n\n.depth-carousel__tint {\n  position: absolute;\n  inset: 0;\n  opacity: 0;\n  pointer-events: none;\n  mix-blend-mode: multiply;\n}\n\n.depth-carousel__arrow {\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 3000;\n  width: 42px;\n  height: 42px;\n  display: grid;\n  place-items: center;\n  border: 1px solid rgba(255, 255, 255, 0.18);\n  border-radius: 999px;\n  background: rgba(18, 20, 26, 0.55);\n  backdrop-filter: blur(8px);\n  color: #fff;\n  cursor: pointer;\n  transition:\n    background 0.2s ease,\n    border-color 0.2s ease,\n    transform 0.2s ease;\n}\n\n.depth-carousel__arrow:hover {\n  background: rgba(28, 31, 40, 0.85);\n  border-color: rgba(255, 255, 255, 0.4);\n}\n\n.depth-carousel__arrow:active {\n  transform: translateY(-50%) scale(0.94);\n}\n\n.depth-carousel__arrow--prev {\n  left: 16px;\n}\n\n.depth-carousel__arrow--next {\n  right: 16px;\n}\n\n.depth-carousel__dots {\n  position: absolute;\n  bottom: 16px;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 3000;\n  display: flex;\n  gap: 8px;\n  padding: 8px 12px;\n  border-radius: 999px;\n  background: rgba(14, 16, 22, 0.4);\n  backdrop-filter: blur(6px);\n}\n\n.depth-carousel__dot {\n  width: 7px;\n  height: 7px;\n  padding: 0;\n  border: none;\n  border-radius: 999px;\n  background: rgba(255, 255, 255, 0.32);\n  cursor: pointer;\n  transition:\n    width 0.25s ease,\n    background 0.25s ease;\n}\n\n.depth-carousel__dot.is-active {\n  width: 20px;\n  background: #fff;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .depth-carousel__card {\n    will-change: auto;\n  }\n  .depth-carousel__arrow,\n  .depth-carousel__dot {\n    transition: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "DepthCarousel.tsx",
			"content": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  CSSProperties,\n  PointerEvent as ReactPointerEvent,\n  KeyboardEvent as ReactKeyboardEvent\n} from 'react';\nimport gsap from 'gsap';\nimport './DepthCarousel.css';\n\nexport type DepthCarouselItem = string | { image: string; alt?: string };\ntype TiltDirection = 'left' | 'right';\n\nexport interface DepthCarouselProps {\n  items?: DepthCarouselItem[];\n  cardWidth?: number;\n  cardHeight?: number;\n  radius?: number;\n  tint?: string;\n  depth?: number;\n  spread?: number;\n  tilt?: number;\n  tiltDirection?: TiltDirection;\n  perspective?: number;\n  visibleCards?: number;\n  falloff?: number;\n  blur?: number;\n  duration?: number;\n  ease?: string;\n  autoplay?: boolean;\n  autoplayDelay?: number;\n  loop?: boolean;\n  showControls?: boolean;\n  showIndicators?: boolean;\n  onChange?: (index: number, item: { image: string; alt?: string }) => void;\n  className?: string;\n}\n\ninterface CarouselConfig {\n  count: number;\n  depth: number;\n  spread: number;\n  tilt: number;\n  tiltDirection: TiltDirection;\n  visibleCards: number;\n  falloff: number;\n  blur: number;\n  duration: number;\n  ease: string;\n  loop: boolean;\n  cardWidth: number;\n  autoplayDelay: number;\n}\n\ninterface DragState {\n  x: number;\n  startPos: number;\n  lastX: number;\n  lastT: number;\n  v: number;\n  moved: boolean;\n  id: number;\n}\n\nconst DEFAULT_ITEMS: DepthCarouselItem[] = [\n  { image: 'https://picsum.photos/seed/depth1/800/1000', alt: 'Slide 1' },\n  { image: 'https://picsum.photos/seed/depth2/800/1000', alt: 'Slide 2' },\n  { image: 'https://picsum.photos/seed/depth3/800/1000', alt: 'Slide 3' },\n  { image: 'https://picsum.photos/seed/depth4/800/1000', alt: 'Slide 4' },\n  { image: 'https://picsum.photos/seed/depth5/800/1000', alt: 'Slide 5' },\n  { image: 'https://picsum.photos/seed/depth6/800/1000', alt: 'Slide 6' }\n];\n\nconst clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max);\nconst normalizeItem = (it: DepthCarouselItem) => (typeof it === 'string' ? { image: it, alt: '' } : it);\n\nconst DepthCarousel = ({\n  items = DEFAULT_ITEMS,\n  cardWidth = 300,\n  cardHeight = 380,\n  radius = 18,\n  tint = '#05060a',\n  depth = 220,\n  spread = 90,\n  tilt = 22,\n  tiltDirection = 'right',\n  perspective = 1400,\n  visibleCards = 4,\n  falloff = 0.2,\n  blur = 6,\n  duration = 700,\n  ease = 'power3.out',\n  autoplay = false,\n  autoplayDelay = 3200,\n  loop = true,\n  showControls = true,\n  showIndicators = true,\n  onChange,\n  className = ''\n}: DepthCarouselProps) => {\n  const data = useMemo(() => (Array.isArray(items) ? items : []).map(normalizeItem), [items]);\n  const count = data.length;\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const stageRef = useRef<HTMLDivElement | null>(null);\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const overlayRefs = useRef<(HTMLSpanElement | null)[]>([]);\n\n  const posRef = useRef(0);\n  const focusRef = useRef(0);\n  const tweenRef = useRef<gsap.core.Tween | null>(null);\n  const scaleRef = useRef(1);\n  const cfgRef = useRef<CarouselConfig>({} as CarouselConfig);\n  const onChangeRef = useRef(onChange);\n\n  const dragRef = useRef<DragState | null>(null);\n  const wheelTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const autoTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n  const reducedRef = useRef(false);\n\n  const [active, setActive] = useState(0);\n\n  onChangeRef.current = onChange;\n  cfgRef.current = {\n    count,\n    depth,\n    spread,\n    tilt,\n    tiltDirection,\n    visibleCards,\n    falloff,\n    blur,\n    duration,\n    ease,\n    loop,\n    cardWidth,\n    autoplayDelay\n  };\n\n  const layout = useCallback((pos: number) => {\n    const cfg = cfgRef.current;\n    const n = cfg.count;\n    if (!n) return;\n    const dir = cfg.tiltDirection === 'left' ? -1 : 1;\n    const sc = scaleRef.current;\n\n    for (let i = 0; i < n; i++) {\n      const el = cardRefs.current[i];\n      if (!el) continue;\n\n      let d = i - pos;\n      if (cfg.loop && n > 1) {\n        d = ((d % n) + n) % n;\n        if (d > n / 2) d -= n;\n      }\n\n      const back = Math.max(0, d);\n      const az = Math.abs(d);\n      const shown = az <= cfg.visibleCards + 0.5;\n\n      const tz = -cfg.depth * d;\n      const tx = dir * cfg.spread * d;\n      const ry = dir * cfg.tilt * clamp(d, 0, 1);\n\n      let opacity = d < 0 ? Math.max(0, 1 + d) : 1;\n      if (!shown) opacity = 0;\n\n      const brightness = Math.max(0.15, 1 - back * cfg.falloff);\n      const blurPx = cfg.blur > 0 ? Math.min(cfg.blur, (back / Math.max(1, cfg.visibleCards)) * cfg.blur) : 0;\n      const zi = Math.round(2000 - d * 20);\n\n      el.style.transform = `translate(-50%, -50%) scale(${sc}) translateX(${tx.toFixed(2)}px) translateZ(${tz.toFixed(2)}px) rotateY(${ry.toFixed(3)}deg)`;\n      el.style.opacity = opacity.toFixed(3);\n      el.style.filter = `brightness(${brightness.toFixed(3)}) blur(${blurPx.toFixed(2)}px)`;\n      el.style.zIndex = String(zi);\n      el.style.pointerEvents = shown && opacity > 0.05 ? 'auto' : 'none';\n\n      const ov = overlayRefs.current[i];\n      if (ov) ov.style.opacity = clamp(back * cfg.falloff * 1.25, 0, 0.86).toFixed(3);\n    }\n  }, []);\n\n  const notify = useCallback(\n    (idx: number) => {\n      setActive(idx);\n      onChangeRef.current?.(idx, data[idx]);\n    },\n    [data]\n  );\n\n  const tweenTo = useCallback(\n    (target: number, animate: boolean) => {\n      tweenRef.current?.kill();\n      const cfg = cfgRef.current;\n      const proxy = { p: posRef.current };\n      const dur = animate && !reducedRef.current ? cfg.duration / 1000 : 0;\n      tweenRef.current = gsap.to(proxy, {\n        p: target,\n        duration: dur,\n        ease: cfg.ease,\n        onUpdate: () => {\n          posRef.current = proxy.p;\n          layout(proxy.p);\n        },\n        onComplete: () => {\n          const n = cfg.count;\n          if (n > 0) posRef.current = ((posRef.current % n) + n) % n;\n          layout(posRef.current);\n        }\n      });\n    },\n    [layout]\n  );\n\n  const setFocus = useCallback(\n    (rawIndex: number, animate = true) => {\n      const cfg = cfgRef.current;\n      const n = cfg.count;\n      if (!n) return;\n      const idx = cfg.loop ? ((rawIndex % n) + n) % n : clamp(rawIndex, 0, n - 1);\n      let delta = idx - posRef.current;\n      if (cfg.loop && n > 1) {\n        delta = ((delta % n) + n) % n;\n        if (delta > n / 2) delta -= n;\n      }\n      tweenTo(posRef.current + delta, animate);\n      if (idx !== focusRef.current) {\n        focusRef.current = idx;\n        notify(idx);\n      }\n    },\n    [tweenTo, notify]\n  );\n\n  const navigateBy = useCallback((step: number) => setFocus(focusRef.current + step, true), [setFocus]);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const ro = new ResizeObserver(entries => {\n      const w = entries[0].contentRect.width;\n      const cfg = cfgRef.current;\n      const needed = cfg.cardWidth + Math.abs(cfg.spread) * 2 + 120;\n      scaleRef.current = clamp(w / needed, 0.4, 1);\n      layout(posRef.current);\n    });\n    ro.observe(root);\n    return () => ro.disconnect();\n  }, [layout]);\n\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const onWheel = (e: WheelEvent) => {\n      const cfg = cfgRef.current;\n      if (cfg.count < 2) return;\n      e.preventDefault();\n      tweenRef.current?.kill();\n      const raw = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;\n      const delta = e.deltaMode === 1 ? raw * 24 : raw;\n      const step = clamp(delta / (cfg.cardWidth * 0.9), -0.6, 0.6);\n      posRef.current += step;\n      layout(posRef.current);\n      if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n      wheelTimerRef.current = setTimeout(() => setFocus(Math.round(posRef.current), true), 130);\n    };\n    el.addEventListener('wheel', onWheel, { passive: false });\n    return () => {\n      el.removeEventListener('wheel', onWheel);\n      if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n    };\n  }, [layout, setFocus]);\n\n  const onPointerDown = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {\n    const cfg = cfgRef.current;\n    if (cfg.count < 2) return;\n    tweenRef.current?.kill();\n    dragRef.current = {\n      x: e.clientX,\n      startPos: posRef.current,\n      lastX: e.clientX,\n      lastT: performance.now(),\n      v: 0,\n      moved: false,\n      id: e.pointerId\n    };\n  }, []);\n\n  const onPointerMove = useCallback(\n    (e: ReactPointerEvent<HTMLDivElement>) => {\n      const drag = dragRef.current;\n      if (!drag) return;\n      const cfg = cfgRef.current;\n      const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n      const dx = e.clientX - drag.x;\n      if (!drag.moved && Math.abs(dx) > 4) {\n        drag.moved = true;\n        rootRef.current?.setPointerCapture(drag.id);\n      }\n      if (!drag.moved) return;\n      const now = performance.now();\n      const dt = Math.max(now - drag.lastT, 1);\n      drag.v = (e.clientX - drag.lastX) / dt;\n      drag.lastX = e.clientX;\n      drag.lastT = now;\n      posRef.current = drag.startPos - dx / stepPx;\n      layout(posRef.current);\n    },\n    [layout]\n  );\n\n  const onPointerEnd = useCallback(() => {\n    const drag = dragRef.current;\n    if (!drag) return;\n    dragRef.current = null;\n    if (!drag.moved) return;\n    const cfg = cfgRef.current;\n    const stepPx = Math.max(cfg.cardWidth * 0.55 * scaleRef.current, 40);\n    const projected = posRef.current - (drag.v * 180) / stepPx;\n    setFocus(Math.round(projected), true);\n  }, [setFocus]);\n\n  const onKeyDown = useCallback(\n    (e: ReactKeyboardEvent<HTMLDivElement>) => {\n      if (e.key === 'ArrowLeft') {\n        e.preventDefault();\n        navigateBy(-1);\n      } else if (e.key === 'ArrowRight') {\n        e.preventDefault();\n        navigateBy(1);\n      }\n    },\n    [navigateBy]\n  );\n\n  const onCardClick = useCallback(\n    (index: number) => {\n      if (dragRef.current?.moved) return;\n      setFocus(index, true);\n    },\n    [setFocus]\n  );\n\n  useEffect(() => {\n    reducedRef.current = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n    if (!autoplay || reducedRef.current || count < 2) return;\n    const root = rootRef.current;\n    let hovered = false;\n    let focused = false;\n    const stop = () => {\n      if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n      autoTimerRef.current = null;\n    };\n    const start = () => {\n      stop();\n      autoTimerRef.current = setInterval(\n        () => {\n          if (!hovered && !focused) navigateBy(1);\n        },\n        Math.max(cfgRef.current.autoplayDelay, 600)\n      );\n    };\n    const onEnter = () => {\n      hovered = true;\n    };\n    const onLeave = () => {\n      hovered = false;\n    };\n    const onFocusIn = () => {\n      focused = true;\n    };\n    const onFocusOut = () => {\n      focused = false;\n    };\n    root?.addEventListener('mouseenter', onEnter);\n    root?.addEventListener('mouseleave', onLeave);\n    root?.addEventListener('focusin', onFocusIn);\n    root?.addEventListener('focusout', onFocusOut);\n    start();\n    return () => {\n      stop();\n      root?.removeEventListener('mouseenter', onEnter);\n      root?.removeEventListener('mouseleave', onLeave);\n      root?.removeEventListener('focusin', onFocusIn);\n      root?.removeEventListener('focusout', onFocusOut);\n    };\n  }, [autoplay, autoplayDelay, count, navigateBy]);\n\n  useEffect(() => {\n    layout(posRef.current);\n  }, [layout, depth, spread, tilt, tiltDirection, visibleCards, falloff, blur, cardWidth, cardHeight, radius, count]);\n\n  useEffect(\n    () => () => {\n      tweenRef.current?.kill();\n      if (wheelTimerRef.current) clearTimeout(wheelTimerRef.current);\n      if (autoTimerRef.current) clearInterval(autoTimerRef.current);\n    },\n    []\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      className={`depth-carousel ${className}`.trim()}\n      style={{ '--dc-perspective': `${perspective}px` } as CSSProperties}\n      role=\"group\"\n      aria-roledescription=\"carousel\"\n      aria-label=\"Depth carousel\"\n      tabIndex={0}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={onPointerEnd}\n      onPointerCancel={onPointerEnd}\n      onKeyDown={onKeyDown}\n    >\n      <div className=\"depth-carousel__stage\" ref={stageRef}>\n        {data.map((item, i) => (\n          <div\n            key={i}\n            className=\"depth-carousel__card\"\n            ref={el => {\n              cardRefs.current[i] = el;\n            }}\n            style={{ width: cardWidth, height: cardHeight, borderRadius: radius }}\n            aria-roledescription=\"slide\"\n            aria-label={`${i + 1} of ${count}`}\n            aria-hidden={active !== i}\n            onClick={() => onCardClick(i)}\n          >\n            <img className=\"depth-carousel__img\" src={item.image} alt={item.alt || ''} draggable={false} />\n            <span\n              className=\"depth-carousel__tint\"\n              ref={el => {\n                overlayRefs.current[i] = el;\n              }}\n              style={{ background: tint }}\n            />\n          </div>\n        ))}\n      </div>\n\n      {showControls && count > 1 && (\n        <>\n          <button\n            type=\"button\"\n            className=\"depth-carousel__arrow depth-carousel__arrow--prev\"\n            aria-label=\"Previous slide\"\n            onClick={() => navigateBy(-1)}\n          >\n            <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" 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\n            type=\"button\"\n            className=\"depth-carousel__arrow depth-carousel__arrow--next\"\n            aria-label=\"Next slide\"\n            onClick={() => navigateBy(1)}\n          >\n            <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" 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        </>\n      )}\n\n      {showIndicators && count > 1 && (\n        <div className=\"depth-carousel__dots\" role=\"tablist\" aria-label=\"Slides\">\n          {data.map((_, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              role=\"tab\"\n              aria-selected={active === i}\n              aria-label={`Go to slide ${i + 1}`}\n              className={`depth-carousel__dot${active === i ? ' is-active' : ''}`}\n              onClick={() => setFocus(i, true)}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default DepthCarousel;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}