{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DepthCarousel-TS-TW",
	"title": "DepthCarousel",
	"description": "Cards recede into depth on a 3D rail, with drag, keyboard and auto-advance.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "DepthCarousel/DepthCarousel.tsx",
			"content": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  PointerEvent as ReactPointerEvent,\n  KeyboardEvent as ReactKeyboardEvent\n} from 'react';\nimport gsap from 'gsap';\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={`relative flex h-full min-h-[320px] w-full cursor-grab touch-pan-y select-none items-center justify-center outline-none [perspective-origin:50%_50%] active:cursor-grabbing focus-visible:rounded-xl focus-visible:outline-2 focus-visible:outline-white/50 focus-visible:[outline-offset:4px] ${className}`.trim()}\n      style={{ perspective: `${perspective}px` }}\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=\"absolute inset-0 [transform-style:preserve-3d]\" ref={stageRef}>\n        {data.map((item, i) => (\n          <div\n            key={i}\n            className=\"absolute left-1/2 top-1/2 cursor-pointer overflow-hidden bg-[#0b0d12] shadow-[0_30px_60px_-20px_rgba(0,0,0,0.65),0_8px_20px_-10px_rgba(0,0,0,0.5)] [transform:translate(-50%,-50%)] [transform-origin:center] [will-change:transform,opacity,filter]\"\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\n              className=\"block h-full w-full select-none object-cover [pointer-events:none] [-webkit-user-drag:none]\"\n              src={item.image}\n              alt={item.alt || ''}\n              draggable={false}\n            />\n            <span\n              className=\"pointer-events-none absolute inset-0 opacity-0 mix-blend-multiply\"\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=\"absolute left-4 top-1/2 z-[3000] grid h-[42px] w-[42px] -translate-y-1/2 place-items-center rounded-full border border-white/20 bg-[rgba(18,20,26,0.55)] text-white backdrop-blur-md transition-[background,border-color,transform] duration-200 hover:border-white/40 hover:bg-[rgba(28,31,40,0.85)] active:scale-95\"\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=\"absolute right-4 top-1/2 z-[3000] grid h-[42px] w-[42px] -translate-y-1/2 place-items-center rounded-full border border-white/20 bg-[rgba(18,20,26,0.55)] text-white backdrop-blur-md transition-[background,border-color,transform] duration-200 hover:border-white/40 hover:bg-[rgba(28,31,40,0.85)] active:scale-95\"\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\n          className=\"absolute bottom-4 left-1/2 z-[3000] flex -translate-x-1/2 gap-2 rounded-full bg-[rgba(14,16,22,0.4)] px-3 py-2 backdrop-blur-sm\"\n          role=\"tablist\"\n          aria-label=\"Slides\"\n        >\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={`h-[7px] cursor-pointer rounded-full transition-[width,background] duration-[250ms] ${\n                active === i ? 'w-5 bg-white' : 'w-[7px] bg-white/30'\n              }`}\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"
	]
}