{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "CircularCarousel-TS-TW",
	"title": "CircularCarousel",
	"description": "A 3D ring of images with bendable cards, momentum drag, snapping and click to focus.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "CircularCarousel/CircularCarousel.tsx",
			"content": "'use client';\n\nimport React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nexport interface CircularCarouselItem {\n  src: string;\n  alt?: string;\n  title?: string;\n  subtitle?: string;\n}\n\nexport type CircularCarouselPreset = 'cylinder' | 'orbit' | 'wheel' | 'panorama';\nexport type CircularCarouselIntro = 'assemble' | 'rise' | 'spin' | 'none';\nexport type CircularCarouselAutoplay = 'drift' | 'step' | 'off';\n\nexport interface CircularCarouselProps {\n  items?: CircularCarouselItem[];\n  preset?: CircularCarouselPreset;\n  intro?: CircularCarouselIntro;\n  cardWidth?: number;\n  aspectRatio?: number;\n  gap?: number;\n  curve?: number;\n  tilt?: number;\n  perspective?: number;\n  autoplay?: CircularCarouselAutoplay;\n  speed?: number;\n  interval?: number;\n  direction?: 'left' | 'right';\n  draggable?: boolean;\n  momentum?: number;\n  snap?: boolean;\n  pauseOnHover?: boolean;\n  focusOnClick?: boolean;\n  parallax?: number;\n  stretch?: number;\n  depthFade?: number;\n  fadeColor?: string;\n  innerShade?: number;\n  cornerRadius?: number;\n  captions?: boolean;\n  onChange?: (index: number) => void;\n  onItemClick?: (item: CircularCarouselItem, index: number) => void;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\ntype Vec3 = [number, number, number];\n\ninterface Layout {\n  axis: 'x' | 'y';\n  tilt: number;\n  perspective: number;\n  curve: number;\n  spread: number;\n  inward: boolean;\n  billboard: boolean;\n  backfaces: boolean;\n  window: number;\n}\n\ninterface Tile {\n  index: number;\n  total: number;\n  start: number;\n  end: number;\n  size: number;\n  move: string;\n}\n\ninterface Sample {\n  time: number;\n  angle: number;\n}\n\ninterface Press {\n  id: number;\n  x: number;\n  y: number;\n  angle: number;\n  moved: boolean;\n  origin: number;\n  samples: Sample[];\n}\n\ninterface CarouselState {\n  angle: number;\n  velocity: number;\n  target: number | null;\n  dir: number;\n  press: Press | null;\n  drag: boolean;\n  hover: boolean;\n  pointer: { inside: boolean; x: number; y: number };\n  yaw: number;\n  pitch: number;\n  intro: { type: CircularCarouselIntro; start: number } | null;\n  introDone: boolean;\n  holdUntil: number;\n  stepAt: number;\n  suppressClick: boolean;\n  wheelTimer: ReturnType<typeof setTimeout> | undefined;\n  fit: number;\n  shift: number;\n  drop: number;\n  last: number;\n}\n\ninterface Settings {\n  count: number;\n  step: number;\n  radius: number;\n  layout: Layout;\n  axis: 'x' | 'y';\n  tilt: number;\n  perspective: number;\n  cardW: number;\n  cardH: number;\n  intro: CircularCarouselIntro;\n  autoplay: CircularCarouselAutoplay;\n  speed: number;\n  interval: number;\n  draggable: boolean;\n  momentum: number;\n  snap: boolean;\n  pauseOnHover: boolean;\n  parallax: number;\n  stretch: number;\n  depthFade: number;\n  captions: boolean;\n  reduced: boolean;\n}\n\ninterface IntroPose {\n  radius: number;\n  lift: number;\n}\n\ninterface Leaving {\n  id: number;\n  items: CircularCarouselItem[];\n  delays: number[];\n}\n\nconst STYLE = `\n@keyframes circular-carousel-title { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }\n@keyframes circular-carousel-leave { from { opacity: 1; } to { opacity: 0; } }\n@keyframes circular-carousel-reveal { from { opacity: 0; } to { opacity: 1; } }\n`;\n\nconst MASKS: Partial<Record<CircularCarouselPreset, string>> = {\n  wheel: 'linear-gradient(to bottom, transparent, #000000 18%, #000000 82%, transparent)',\n  panorama: 'linear-gradient(to right, transparent, #000000 12%, #000000 88%, transparent)'\n};\n\nconst photo = (id: string) => `https://images.unsplash.com/${id}?w=900&q=80&auto=format&fit=max&sat=-100`;\n\nconst DEFAULT_ITEMS: CircularCarouselItem[] = [\n  {\n    src: photo('photo-1506744038136-46273834b3fb'),\n    alt: 'Mist drifting through a mountain valley',\n    title: 'Valley',\n    subtitle: 'Landscape'\n  },\n  {\n    src: photo('photo-1524504388940-b1c1722653e1'),\n    alt: 'A woman with long hair in soft studio light',\n    title: 'Portrait',\n    subtitle: 'Studio'\n  },\n  {\n    src: photo('photo-1486406146926-c627a92ad1ab'),\n    alt: 'Glass towers seen from street level',\n    title: 'Towers',\n    subtitle: 'Architecture'\n  },\n  {\n    src: photo('photo-1502680390469-be75c86b636f'),\n    alt: 'A surfer carving inside a breaking wave',\n    title: 'Swell',\n    subtitle: 'Ocean'\n  },\n  {\n    src: photo('photo-1487958449943-2429e8be8625'),\n    alt: 'An angular white building against the sky',\n    title: 'Facade',\n    subtitle: 'Architecture'\n  },\n  {\n    src: photo('photo-1509631179647-0177331693ae'),\n    alt: 'A model in striped trousers leaning on a wall',\n    title: 'Pose',\n    subtitle: 'Editorial'\n  },\n  {\n    src: photo('photo-1519681393784-d120267933ba'),\n    alt: 'The Milky Way above snowy peaks',\n    title: 'Night',\n    subtitle: 'Sky'\n  },\n  {\n    src: photo('photo-1485968579580-b6d095142e6e'),\n    alt: 'A woman in a long coat on a city street',\n    title: 'Street',\n    subtitle: 'Editorial'\n  },\n  {\n    src: photo('photo-1493246507139-91e8fad9978e'),\n    alt: 'Mountains mirrored in a still lake',\n    title: 'Mirror',\n    subtitle: 'Landscape'\n  },\n  {\n    src: photo('photo-1511818966892-d7d671e672a2'),\n    alt: 'A glass skyscraper under a cloudy sky',\n    title: 'Glass',\n    subtitle: 'Architecture'\n  }\n];\n\nconst PRESETS: Record<CircularCarouselPreset, Layout> = {\n  cylinder: {\n    axis: 'y',\n    tilt: -5,\n    perspective: 2500,\n    curve: 1,\n    spread: 1,\n    inward: false,\n    billboard: false,\n    backfaces: true,\n    window: 0\n  },\n  orbit: {\n    axis: 'y',\n    tilt: -16,\n    perspective: 1500,\n    curve: 0,\n    spread: 1.45,\n    inward: false,\n    billboard: true,\n    backfaces: false,\n    window: 0\n  },\n  wheel: {\n    axis: 'x',\n    tilt: 0,\n    perspective: 1800,\n    curve: 0,\n    spread: 1,\n    inward: false,\n    billboard: false,\n    backfaces: true,\n    window: 1.7\n  },\n  panorama: {\n    axis: 'y',\n    tilt: 0,\n    perspective: 0,\n    curve: 1,\n    spread: 1,\n    inward: true,\n    billboard: false,\n    backfaces: false,\n    window: 0\n  }\n};\n\nconst INTRO_LENGTH: Record<CircularCarouselIntro, number> = { assemble: 1500, rise: 1400, spin: 1800, none: 0 };\nconst TILES = 8;\nconst OVERLAP = 2.5;\nconst DRAG_THRESHOLD = 5;\nconst SPRING = 118;\nconst SETTLE_SPEED = 9;\nconst CAPTION_SPACE = 76;\nconst SWAP_LENGTH = 700;\nconst SWAP_SPREAD = 420;\nconst TO_RAD = Math.PI / 180;\n\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\nconst wrap = (degrees: number) => ((((degrees + 180) % 360) + 360) % 360) - 180;\nconst easeOut = (t: number) => 1 - Math.pow(1 - t, 4);\nconst easeOutQuint = (t: number) => 1 - Math.pow(1 - t, 5);\n\nconst rotateX = (p: Vec3, degrees: number): Vec3 => {\n  const r = degrees * TO_RAD;\n  const c = Math.cos(r);\n  const s = Math.sin(r);\n  return [p[0], p[1] * c - p[2] * s, p[1] * s + p[2] * c];\n};\n\nconst rotateY = (p: Vec3, degrees: number): Vec3 => {\n  const r = degrees * TO_RAD;\n  const c = Math.cos(r);\n  const s = Math.sin(r);\n  return [p[0] * c + p[2] * s, p[1], -p[0] * s + p[2] * c];\n};\n\nconst Digits = ({ value }: { value: number }) => (\n  <span className=\"inline-flex\">\n    {String(value)\n      .padStart(2, '0')\n      .split('')\n      .map((digit, index) => (\n        <span key={index} className=\"inline-block h-[1em] overflow-hidden\">\n          <span\n            className=\"flex flex-col transition-transform duration-[600ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none\"\n            style={{ transform: `translateY(${-Number(digit) * 10}%)` }}\n          >\n            {'0123456789'.split('').map(n => (\n              <span key={n} className=\"h-[1em]\">\n                {n}\n              </span>\n            ))}\n          </span>\n        </span>\n      ))}\n  </span>\n);\n\nconst usePrefersReducedMotion = () => {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const query = window.matchMedia?.('(prefers-reduced-motion: reduce)');\n    if (!query) return undefined;\n    const update = () => setReduced(query.matches);\n    update();\n    query.addEventListener?.('change', update);\n    return () => query.removeEventListener?.('change', update);\n  }, []);\n  return reduced;\n};\n\nconst CircularCarousel = ({\n  items = DEFAULT_ITEMS,\n  preset = 'cylinder',\n  intro = 'rise',\n  cardWidth = 220,\n  aspectRatio = 1,\n  gap = 25,\n  curve,\n  tilt,\n  perspective,\n  autoplay = 'drift',\n  speed = 14,\n  interval = 3,\n  direction = 'left',\n  draggable = true,\n  momentum = 0.6,\n  snap = true,\n  pauseOnHover = true,\n  focusOnClick = true,\n  parallax = 0.3,\n  stretch = 0.5,\n  depthFade = 0.55,\n  fadeColor = '#000000',\n  innerShade = 0.6,\n  cornerRadius = 12,\n  captions = false,\n  onChange,\n  onItemClick,\n  className = '',\n  style\n}: CircularCarouselProps) => {\n  const incoming = items && items.length ? items : DEFAULT_ITEMS;\n  const [list, setList] = useState<CircularCarouselItem[]>(incoming);\n  const [leaving, setLeaving] = useState<Leaving | null>(null);\n  const count = list.length;\n  const shape: CircularCarouselPreset = PRESETS[preset] ? preset : 'cylinder';\n  const layout = PRESETS[shape];\n  const axis = layout.axis;\n  const tiltValue = tilt ?? layout.tilt;\n  const curveValue = layout.billboard ? 0 : clamp(curve ?? layout.curve, 0, 1);\n  const reduced = usePrefersReducedMotion();\n\n  const cardW = Math.max(40, cardWidth);\n  const cardH = cardW / clamp(aspectRatio, 0.2, 5);\n  const along = axis === 'x' ? cardH : cardW;\n  const step = 360 / count;\n\n  const radius = useMemo(() => {\n    const n = Math.max(count, 3);\n    const pitch = (along + gap) * layout.spread;\n    const chord = pitch / (2 * Math.sin(Math.PI / n));\n    const arc = (n * pitch) / (2 * Math.PI);\n    return Math.max(chord + (arc - chord) * curveValue, along * 0.6);\n  }, [count, along, gap, curveValue, layout.spread]);\n\n  const tiles = useMemo<Tile[]>(() => {\n    const total = curveValue > 0.001 ? TILES : 1;\n    const length = along / total;\n    const bend = curveValue > 0.001 ? radius / curveValue : 0;\n    return Array.from({ length: total }, (_, index) => {\n      const start = index * length - (index > 0 ? OVERLAP / 2 : 0);\n      const end = (index + 1) * length + (index < total - 1 ? OVERLAP / 2 : 0);\n      const center = (start + end) / 2 - along / 2;\n      const alpha = bend ? center / bend : 0;\n      const shift = bend ? bend * Math.sin(alpha) : center;\n      const sink = bend ? bend * (1 - Math.cos(alpha)) : 0;\n      const depth = layout.inward ? sink : -sink;\n      const turn = ((layout.inward ? -alpha : alpha) * 180) / Math.PI;\n      const move =\n        axis === 'x'\n          ? `translate3d(0px, ${shift}px, ${depth}px) rotateX(${-turn}deg)`\n          : `translate3d(${shift}px, 0px, ${depth}px) rotateY(${turn}deg)`;\n      return { index, total, start, end, size: end - start, move };\n    });\n  }, [along, axis, curveValue, layout.inward, radius]);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const stageRef = useRef<HTMLDivElement>(null);\n  const cameraRef = useRef<HTMLDivElement>(null);\n  const ringRef = useRef<HTMLDivElement>(null);\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n  const wakeRef = useRef<() => void>(() => {});\n  const measureRef = useRef<() => void>(() => {});\n  const activeRef = useRef(0);\n  const [active, setActive] = useState(0);\n  const [ready, setReady] = useState(false);\n  const [dragging, setDragging] = useState(false);\n  const readyRef = useRef(false);\n  readyRef.current = ready;\n\n  const stateRef = useRef<CarouselState>({\n    angle: 0,\n    velocity: 0,\n    target: null,\n    dir: 0,\n    press: null,\n    drag: false,\n    hover: false,\n    pointer: { inside: false, x: 0, y: 0 },\n    yaw: 0,\n    pitch: 0,\n    intro: null,\n    introDone: false,\n    holdUntil: 0,\n    stepAt: 0,\n    suppressClick: false,\n    wheelTimer: undefined,\n    fit: 1,\n    shift: 0,\n    drop: 0,\n    last: 0\n  });\n\n  const settings: Settings = {\n    count,\n    step,\n    radius,\n    layout,\n    axis,\n    tilt: tiltValue,\n    perspective: layout.inward ? radius : (perspective ?? layout.perspective),\n    cardW,\n    cardH,\n    intro: reduced ? 'none' : intro in INTRO_LENGTH ? intro : 'rise',\n    autoplay: reduced ? 'off' : autoplay,\n    speed,\n    interval: Math.max(0.5, interval),\n    draggable,\n    momentum: clamp(momentum, 0, 1),\n    snap,\n    pauseOnHover,\n    parallax: reduced ? 0 : clamp(parallax, 0, 1),\n    stretch: reduced ? 0 : clamp(stretch, 0, 1),\n    depthFade: clamp(depthFade, 0, 1),\n    captions,\n    reduced\n  };\n  const settingsRef = useRef(settings);\n  settingsRef.current = settings;\n\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n\n  const dragSign = layout.inward ? -1 : 1;\n  const directionSign = (direction === 'right' ? 1 : -1) * dragSign;\n\n  useEffect(() => {\n    stateRef.current.dir = directionSign;\n    wakeRef.current();\n  }, [directionSign]);\n\n  const incomingRef = useRef(incoming);\n  incomingRef.current = incoming;\n  const listRef = useRef(list);\n  listRef.current = list;\n  const sourcesKey = incoming.map(item => item.src).join('|');\n\n  useEffect(() => {\n    let cancelled = false;\n    const next = incomingRef.current;\n    const smooth = readyRef.current && next.length === listRef.current.length;\n    if (!smooth) {\n      setReady(false);\n      setLeaving(null);\n      setList(next);\n    }\n    const sources = next.slice(0, 16).map(item => item.src);\n    const load = (src: string) =>\n      new Promise<void>(resolve => {\n        const image = new Image();\n        image.decoding = 'async';\n        image.onload = () => {\n          if (image.decode)\n            image.decode().then(\n              () => resolve(),\n              () => resolve()\n            );\n          else resolve();\n        };\n        image.onerror = () => resolve();\n        image.src = src;\n      });\n    const timeout = new Promise<void>(resolve => setTimeout(resolve, 2400));\n    Promise.race([Promise.all(sources.map(load)), timeout]).then(() => {\n      if (cancelled) return;\n      const state = stateRef.current;\n      if (smooth) {\n        const previous = listRef.current;\n        const s = settingsRef.current;\n        if (!s.reduced && previous !== next) {\n          setLeaving(current => ({\n            id: (current?.id || 0) + 1,\n            items: previous,\n            delays: previous.map((_, index) => (Math.abs(wrap(index * s.step + state.angle)) / 180) * SWAP_SPREAD)\n          }));\n        }\n        setList(next);\n        wakeRef.current();\n        return;\n      }\n      state.introDone = false;\n      state.intro = null;\n      setReady(true);\n      wakeRef.current();\n    });\n    return () => {\n      cancelled = true;\n    };\n  }, [sourcesKey]);\n\n  useEffect(() => {\n    if (!leaving) return undefined;\n    const timer = setTimeout(() => setLeaving(null), SWAP_LENGTH + SWAP_SPREAD + 80);\n    return () => clearTimeout(timer);\n  }, [leaving]);\n\n  useLayoutEffect(() => {\n    const root = rootRef.current;\n    const stage = stageRef.current;\n    const camera = cameraRef.current;\n    const ring = ringRef.current;\n    if (!root || !stage || !camera || !ring) return undefined;\n    const state = stateRef.current;\n    let raf = 0;\n    let visible = true;\n\n    const nearest = (angle: number) => Math.round(angle / settingsRef.current.step) * settingsRef.current.step;\n\n    const measure = () => {\n      const s = settingsRef.current;\n      const rect = root.getBoundingClientRect();\n      if (!rect.width || !rect.height) return;\n      const room = s.captions ? CAPTION_SPACE : 0;\n      const width = rect.width * 0.94;\n      const height = (rect.height - room) * 0.92;\n      const P = s.perspective;\n      let minX = Infinity;\n      let maxX = -Infinity;\n      let minY = Infinity;\n      let maxY = -Infinity;\n      if (s.layout.inward) {\n        minX = -width / 2;\n        maxX = width / 2;\n        minY = -s.cardH / 2;\n        maxY = s.cardH / 2;\n      } else {\n        const corners: [number, number][] = [\n          [-s.cardW / 2, -s.cardH / 2],\n          [s.cardW / 2, -s.cardH / 2],\n          [-s.cardW / 2, s.cardH / 2],\n          [s.cardW / 2, s.cardH / 2]\n        ];\n        const limit = s.layout.window ? s.layout.window * s.step : 180;\n        for (let a = -limit; a <= limit; a += limit / 24) {\n          for (const [cx, cy] of corners) {\n            let p: Vec3;\n            if (s.axis === 'x') {\n              p = rotateY(rotateX([cx, cy, s.radius], -a), s.tilt);\n            } else if (s.layout.billboard) {\n              const c = rotateY([0, 0, s.radius], a);\n              p = rotateX([c[0] + cx, cy, c[2]], s.tilt);\n            } else {\n              p = rotateX(rotateY([cx, cy, s.radius], a), s.tilt);\n            }\n            p = [p[0], p[1], p[2] - s.radius];\n            if (p[2] >= P * 0.95) continue;\n            const k = P / (P - p[2]);\n            minX = Math.min(minX, p[0] * k);\n            maxX = Math.max(maxX, p[0] * k);\n            minY = Math.min(minY, p[1] * k);\n            maxY = Math.max(maxY, p[1] * k);\n          }\n        }\n      }\n      const spanX = Math.max(maxX - minX, 1);\n      const spanY = Math.max(maxY - minY, 1);\n      const fit = Math.min(1, width / spanX, height / spanY);\n      state.fit = fit;\n      state.shift = -((minY + maxY) / 2) * fit - room / 2;\n      state.drop = s.axis === 'x' ? (rect.width / fit) * 0.55 + s.cardW : (rect.height / fit) * 0.55 + s.cardH;\n      stage.style.perspective = `${P}px`;\n      stage.style.transform = `translate3d(0, ${state.shift}px, 0) scale(${fit})`;\n    };\n    measureRef.current = measure;\n\n    const introCard = (elapsed: number, landing: number): IntroPose => {\n      if (!state.intro) return { radius: 1, lift: 0 };\n      const type = state.intro.type;\n      const reach = Math.abs(wrap(landing + state.angle));\n      if (type === 'assemble') {\n        const delay = (reach / 180) * 420;\n        const p = easeOut(clamp((elapsed - delay) / 1080, 0, 1));\n        return { radius: 1 + 0.6 * (1 - p), lift: 0 };\n      }\n      if (type === 'rise') {\n        const delay = (reach / 180) * 480;\n        const p = easeOutQuint(clamp((elapsed - delay) / 900, 0, 1));\n        return { radius: 1, lift: (1 - p) * state.drop };\n      }\n      if (type === 'spin') {\n        const p = easeOut(clamp(elapsed / INTRO_LENGTH.spin, 0, 1));\n        return { radius: 1 + 0.28 * (1 - p), lift: 0 };\n      }\n      return { radius: 1, lift: 0 };\n    };\n\n    const advance = (s: Settings, dt: number, now: number) => {\n      if (!state.introDone && readyRef.current) {\n        if (!state.intro) {\n          if (s.intro === 'none') state.introDone = true;\n          else state.intro = { type: s.intro, start: now };\n        }\n        if (state.intro && now - state.intro.start >= INTRO_LENGTH[state.intro.type]) {\n          state.intro = null;\n          state.introDone = true;\n        }\n      }\n\n      const paused = (s.pauseOnHover && state.hover) || state.drag || now < state.holdUntil;\n      const cruise = s.autoplay === 'drift' && !paused && !state.intro ? s.speed * state.dir : 0;\n      let busy = Boolean(state.intro) || state.drag;\n\n      if (state.drag || state.intro) {\n        state.velocity = state.drag ? state.velocity : 0;\n      } else if (state.target !== null) {\n        let remaining = dt;\n        const damping = 2 * Math.sqrt(SPRING);\n        while (remaining > 0) {\n          const h = Math.min(remaining, 1 / 240);\n          const accel = SPRING * (state.target - state.angle) - damping * state.velocity;\n          state.velocity += accel * h;\n          state.angle += state.velocity * h;\n          remaining -= h;\n        }\n        if (Math.abs(state.target - state.angle) < 0.004 && Math.abs(state.velocity) < 0.03) {\n          state.angle = state.target;\n          state.velocity = 0;\n          state.target = null;\n        }\n        busy = true;\n      } else {\n        const tau = 0.18 + s.momentum * 1.5;\n        state.velocity += (cruise - state.velocity) * (1 - Math.exp(-dt / tau));\n        state.angle += state.velocity * dt;\n        if (cruise === 0 && s.snap && Math.abs(state.velocity) < SETTLE_SPEED) {\n          state.target = nearest(state.angle);\n        }\n        busy = busy || cruise !== 0 || Math.abs(state.velocity) > 0.01 || state.target !== null;\n      }\n\n      if (s.autoplay === 'step' && !paused && !state.intro && state.introDone) {\n        if (!state.stepAt) state.stepAt = now + s.interval * 1000;\n        if (now >= state.stepAt) {\n          state.target = (state.target ?? nearest(state.angle)) + s.step * state.dir;\n          state.stepAt = now + s.interval * 1000;\n        }\n        busy = true;\n      } else {\n        state.stepAt = 0;\n      }\n\n      if (now < state.holdUntil) busy = true;\n\n      const ease = 1 - Math.exp(-dt / 0.35);\n      const aimYaw = state.pointer.inside ? state.pointer.x * s.parallax * 9 : 0;\n      const aimPitch = state.pointer.inside ? -state.pointer.y * s.parallax * 6 : 0;\n      state.yaw += (aimYaw - state.yaw) * ease;\n      state.pitch += (aimPitch - state.pitch) * ease;\n      if (Math.abs(aimYaw - state.yaw) > 0.01 || Math.abs(aimPitch - state.pitch) > 0.01) busy = true;\n\n      return busy;\n    };\n\n    const render = (s: Settings, now: number) => {\n      const elapsed = state.intro ? now - state.intro.start : 0;\n      const swell = 1 + s.stretch * 0.12 * Math.min(1, Math.abs(state.velocity) / 420);\n      let spinOffset = 0;\n      if (state.intro?.type === 'spin') {\n        const p = easeOut(clamp(elapsed / INTRO_LENGTH.spin, 0, 1));\n        spinOffset = -300 * state.dir * (1 - p);\n      } else if (state.intro?.type === 'assemble') {\n        const p = easeOut(clamp(elapsed / INTRO_LENGTH.assemble, 0, 1));\n        spinOffset = -32 * state.dir * (1 - p);\n      }\n      const angle = state.angle + spinOffset;\n      const R = s.radius * swell;\n\n      if (s.axis === 'x') {\n        camera.style.transform = `translate3d(0, 0, ${-R}px) rotateY(${s.tilt + state.yaw}deg) rotateX(${state.pitch}deg)`;\n        ring.style.transform = `rotateX(${-angle}deg)`;\n      } else if (s.layout.inward) {\n        camera.style.transform = `translate3d(0, 0, ${s.perspective - 1}px) rotateX(${s.tilt + state.pitch}deg) rotateY(${state.yaw}deg)`;\n        ring.style.transform = `rotateY(${angle}deg)`;\n      } else {\n        camera.style.transform = `translate3d(0, 0, ${-R}px) rotateX(${s.tilt + state.pitch}deg) rotateY(${state.yaw}deg)`;\n        ring.style.transform = `rotateY(${angle}deg)`;\n      }\n\n      for (let index = 0; index < s.count; index++) {\n        const card = cardRefs.current[index];\n        if (!card) continue;\n        const base = index * s.step;\n        const mod = introCard(elapsed, base);\n        const r = R * mod.radius;\n        let transform: string;\n        if (s.axis === 'x') {\n          transform = `rotateX(${-base}deg) translateZ(${r}px)`;\n        } else if (s.layout.inward) {\n          transform = `rotateY(${base}deg) translateZ(${-r}px)`;\n        } else {\n          transform = `rotateY(${base}deg) translateZ(${r}px)`;\n          if (s.layout.billboard) transform += ` rotateY(${-(base + angle)}deg)`;\n        }\n        if (mod.lift) transform += s.axis === 'x' ? ` translateX(${mod.lift}px)` : ` translateY(${mod.lift}px)`;\n        card.style.transform = transform;\n\n        const world = wrap(base + angle);\n        const facing = Math.cos(world * TO_RAD);\n        if (s.layout.inward) card.style.visibility = Math.abs(world) > 86 ? 'hidden' : '';\n        const fade = s.depthFade * Math.pow((1 - facing) / 2, 1.25);\n        card.style.setProperty('--cc-depth', fade.toFixed(3));\n      }\n\n      const index = ((Math.round(-state.angle / s.step) % s.count) + s.count) % s.count || 0;\n      if (index !== activeRef.current) {\n        activeRef.current = index;\n        setActive(index);\n        onChangeRef.current?.(index);\n      }\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      const s = settingsRef.current;\n      const dt = state.last ? Math.min((now - state.last) / 1000, 0.05) : 1 / 60;\n      state.last = now;\n      const busy = advance(s, dt, now);\n      render(s, now);\n      if (busy && visible && !document.hidden) raf = requestAnimationFrame(frame);\n      else state.last = 0;\n    };\n\n    const wake = () => {\n      if (!raf && visible && !document.hidden) raf = requestAnimationFrame(frame);\n    };\n    wakeRef.current = wake;\n\n    const onVisibility = () => {\n      if (document.hidden) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n        state.last = 0;\n      } else wake();\n    };\n\n    const resize = new ResizeObserver(() => {\n      measure();\n      wake();\n    });\n    resize.observe(root);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) wake();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0;\n        state.last = 0;\n      }\n    });\n    io.observe(root);\n\n    const onWheel = (event: WheelEvent) => {\n      const s = settingsRef.current;\n      if (!s.draggable) return;\n      const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : 0;\n      if (!delta) return;\n      event.preventDefault();\n      const perPixel = 180 / (Math.PI * s.radius * state.fit);\n      state.target = null;\n      state.angle -= delta * perPixel * (s.layout.inward ? -1 : 1);\n      state.velocity = -delta * perPixel * (s.layout.inward ? -1 : 1) * 30;\n      state.holdUntil = performance.now() + 1600;\n      clearTimeout(state.wheelTimer);\n      state.wheelTimer = setTimeout(() => {\n        if (settingsRef.current.snap) state.target = nearest(state.angle + state.velocity * 0.12);\n        wake();\n      }, 140);\n      wake();\n    };\n    root.addEventListener('wheel', onWheel, { passive: false });\n    document.addEventListener('visibilitychange', onVisibility);\n\n    measure();\n    render(settingsRef.current, performance.now());\n    wake();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      resize.disconnect();\n      io.disconnect();\n      clearTimeout(state.wheelTimer);\n      root.removeEventListener('wheel', onWheel);\n      document.removeEventListener('visibilitychange', onVisibility);\n    };\n  }, []);\n\n  useLayoutEffect(() => {\n    measureRef.current();\n    wakeRef.current();\n  }, [radius, cardW, cardH, tiltValue, perspective, preset, captions, count]);\n\n  useEffect(() => {\n    wakeRef.current();\n  });\n\n  const focusIndex = useCallback((index: number) => {\n    const state = stateRef.current;\n    const s = settingsRef.current;\n    let target = -index * s.step;\n    target += 360 * Math.round((state.angle - target) / 360);\n    state.target = target;\n    state.holdUntil = performance.now() + 2800;\n    wakeRef.current();\n  }, []);\n\n  const stepBy = useCallback((delta: number) => {\n    const state = stateRef.current;\n    const s = settingsRef.current;\n    const base = state.target ?? Math.round(state.angle / s.step) * s.step;\n    state.target = base - delta * s.step * (s.layout.inward ? -1 : 1);\n    state.holdUntil = performance.now() + 2800;\n    wakeRef.current();\n  }, []);\n\n  const updatePointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const root = rootRef.current;\n    if (!root) return;\n    const rect = root.getBoundingClientRect();\n    const pointer = stateRef.current.pointer;\n    pointer.x = clamp(((event.clientX - rect.left) / rect.width) * 2 - 1, -1, 1);\n    pointer.y = clamp(((event.clientY - rect.top) / rect.height) * 2 - 1, -1, 1);\n  };\n\n  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    const state = stateRef.current;\n    state.suppressClick = false;\n    if (!draggable || event.button !== 0) return;\n    state.press = {\n      id: event.pointerId,\n      x: event.clientX,\n      y: event.clientY,\n      angle: state.angle,\n      moved: false,\n      origin: 0,\n      samples: [{ time: performance.now(), angle: state.angle }]\n    };\n  };\n\n  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    const state = stateRef.current;\n    if (event.pointerType === 'mouse') {\n      state.pointer.inside = true;\n      updatePointer(event);\n    }\n    const press = state.press;\n    if (!press || press.id !== event.pointerId) {\n      wakeRef.current();\n      return;\n    }\n    const s = settingsRef.current;\n    const delta = s.axis === 'x' ? event.clientY - press.y : event.clientX - press.x;\n    const cross = s.axis === 'x' ? event.clientX - press.x : event.clientY - press.y;\n    if (!press.moved) {\n      if (Math.abs(delta) < DRAG_THRESHOLD) return;\n      if (Math.abs(cross) > Math.abs(delta) * 1.2 && event.pointerType !== 'mouse') {\n        state.press = null;\n        return;\n      }\n      press.moved = true;\n      press.origin = delta;\n      state.drag = true;\n      state.target = null;\n      state.velocity = 0;\n      setDragging(true);\n      try {\n        rootRef.current?.setPointerCapture(event.pointerId);\n      } catch {}\n    }\n    const perPixel = 180 / (Math.PI * s.radius * state.fit);\n    state.angle = press.angle + (delta - press.origin) * perPixel * (s.layout.inward ? -1 : 1);\n    const now = performance.now();\n    press.samples.push({ time: now, angle: state.angle });\n    while (press.samples.length > 2 && now - press.samples[0].time > 110) press.samples.shift();\n    wakeRef.current();\n  };\n\n  const releasePointer = (event: React.PointerEvent<HTMLDivElement>) => {\n    const state = stateRef.current;\n    const press = state.press;\n    if (!press || press.id !== event.pointerId) return;\n    state.press = null;\n    if (!press.moved) return;\n    state.drag = false;\n    setDragging(false);\n    state.suppressClick = true;\n    const s = settingsRef.current;\n    const first = press.samples[0];\n    const last = press.samples[press.samples.length - 1];\n    const span = (last.time - first.time) / 1000;\n    const velocity = span > 0.008 ? clamp((last.angle - first.angle) / span, -1400, 1400) : 0;\n    state.velocity = velocity;\n    if (Math.abs(velocity) > 60) state.dir = Math.sign(velocity);\n    const coasting = s.autoplay === 'drift' && !(s.pauseOnHover && state.hover && event.pointerType === 'mouse');\n    if (s.snap && !coasting) {\n      const tau = 0.18 + s.momentum * 1.5;\n      state.target = Math.round((state.angle + velocity * tau * 0.55) / s.step) * s.step;\n    }\n    wakeRef.current();\n  };\n\n  const handlePointerEnter = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (event.pointerType !== 'mouse') return;\n    stateRef.current.hover = true;\n    wakeRef.current();\n  };\n\n  const handlePointerLeave = (event: React.PointerEvent<HTMLDivElement>) => {\n    const state = stateRef.current;\n    if (event.pointerType === 'mouse') {\n      state.hover = false;\n      state.pointer.inside = false;\n    }\n    wakeRef.current();\n  };\n\n  const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {\n    const state = stateRef.current;\n    if (state.suppressClick) {\n      state.suppressClick = false;\n      return;\n    }\n    const card = (event.target as HTMLElement).closest?.('[data-cc-index]');\n    if (!card) return;\n    const index = Number(card.getAttribute('data-cc-index'));\n    if (focusOnClick) focusIndex(index);\n    onItemClick?.(list[index], index);\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const forward = axis === 'x' ? 'ArrowDown' : 'ArrowRight';\n    const backward = axis === 'x' ? 'ArrowUp' : 'ArrowLeft';\n    if (event.key === forward) stepBy(1);\n    else if (event.key === backward) stepBy(-1);\n    else if (event.key === 'Home') focusIndex(0);\n    else if (event.key === 'End') focusIndex(count - 1);\n    else if (event.key === 'Enter' || event.key === ' ') onItemClick?.(list[activeRef.current], activeRef.current);\n    else return;\n    event.preventDefault();\n  };\n\n  const current = list[active] || list[0];\n  const label = current ? current.title || current.alt || `Image ${active + 1}` : '';\n\n  const renderTile = (item: CircularCarouselItem, tile: Tile, back: boolean, index: number) => {\n    const strip = back ? tile.total - 1 - tile.index : tile.index;\n    const first = strip === 0;\n    const last = strip === tile.total - 1;\n    const r = 'var(--cc-radius)';\n    const frameRadius =\n      axis === 'x'\n        ? `${first ? r : 0} ${first ? r : 0} ${last ? r : 0} ${last ? r : 0}`\n        : `${first ? r : 0} ${last ? r : 0} ${last ? r : 0} ${first ? r : 0}`;\n    const offset = back ? along - tile.end : tile.start;\n    const size = tile.size;\n    const box: React.CSSProperties =\n      axis === 'x'\n        ? { left: -cardW / 2, top: -size / 2, width: cardW, height: size }\n        : { left: -size / 2, top: -cardH / 2, width: size, height: cardH };\n    const photoStyle: React.CSSProperties =\n      axis === 'x'\n        ? { left: 0, top: -offset, width: cardW, height: cardH }\n        : { left: -offset, top: 0, width: cardW, height: cardH };\n    const flip = axis === 'x' ? ' rotateX(180deg)' : ' rotateY(180deg)';\n    const gone = leaving?.items[index];\n    return (\n      <div\n        key={`${back ? 'b' : 'f'}${tile.index}`}\n        className=\"absolute [-webkit-backface-visibility:hidden] [backface-visibility:hidden] [outline:1px_solid_transparent]\"\n        style={{ ...box, transform: tile.move + (back ? flip : '') }}\n        aria-hidden=\"true\"\n      >\n        <div\n          className=\"absolute left-0 top-0 w-full overflow-hidden\"\n          style={{ height: axis === 'x' ? size : cardH, borderRadius: frameRadius }}\n        >\n          <img\n            className=\"pointer-events-none absolute block max-w-none select-none object-cover [-webkit-user-drag:none]\"\n            src={item.src}\n            alt=\"\"\n            draggable={false}\n            decoding=\"async\"\n            style={photoStyle}\n          />\n          {gone && (\n            <img\n              key={`leaving-${leaving.id}`}\n              className=\"pointer-events-none absolute block max-w-none select-none object-cover [-webkit-user-drag:none] animate-[circular-carousel-leave_700ms_cubic-bezier(0.33,1,0.68,1)_both]\"\n              src={gone.src}\n              alt=\"\"\n              draggable={false}\n              decoding=\"async\"\n              style={{ ...photoStyle, animationDelay: `${Math.round(leaving.delays[index])}ms` }}\n            />\n          )}\n          {back && <div className=\"pointer-events-none absolute inset-0 bg-black opacity-[var(--cc-inner)]\" />}\n          <div className=\"pointer-events-none absolute inset-0 bg-[var(--cc-fade)] opacity-[var(--cc-depth,0)]\" />\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`group/cc relative h-full w-full touch-pan-y select-none overflow-hidden outline-none [-webkit-tap-highlight-color:transparent] focus-visible:shadow-[inset_0_0_0_2px_rgba(128,128,140,0.55)] data-[axis=x]:touch-pan-x data-[draggable]:cursor-grab data-[dragging]:cursor-grabbing ${className}`.trim()}\n      style={\n        {\n          ...style,\n          '--cc-fade': fadeColor,\n          '--cc-radius': `${Math.max(0, cornerRadius)}px`,\n          '--cc-inner': (1 - clamp(innerShade, 0, 1)).toFixed(3)\n        } as React.CSSProperties\n      }\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label=\"Image carousel\"\n      tabIndex={0}\n      data-axis={axis}\n      data-shape={shape}\n      data-ready={ready ? '' : undefined}\n      data-draggable={draggable ? '' : undefined}\n      data-dragging={dragging ? '' : undefined}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={releasePointer}\n      onPointerCancel={releasePointer}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n    >\n      <div\n        className=\"absolute inset-0\"\n        style={MASKS[shape] ? { maskImage: MASKS[shape], WebkitMaskImage: MASKS[shape] } : undefined}\n      >\n        <div\n          ref={stageRef}\n          className=\"absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-[450ms] ease-[ease] group-data-[ready]/cc:opacity-100 motion-reduce:transition-none\"\n        >\n          <div ref={cameraRef} className=\"relative h-0 w-0 [transform-style:preserve-3d]\">\n            <div ref={ringRef} className=\"absolute left-0 top-0 h-0 w-0 [transform-style:preserve-3d]\">\n              {list.map((item, index) => (\n                <div\n                  key={index}\n                  ref={element => {\n                    cardRefs.current[index] = element;\n                  }}\n                  className=\"absolute left-0 top-0 h-0 w-0 [transform-style:preserve-3d]\"\n                  data-cc-index={index}\n                  role=\"group\"\n                  aria-roledescription=\"slide\"\n                  aria-label={`${item.title || item.alt || `Image ${index + 1}`}, ${index + 1} of ${count}`}\n                >\n                  {tiles.map(tile => renderTile(item, tile, false, index))}\n                  {layout.backfaces && tiles.map(tile => renderTile(item, tile, true, index))}\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n      {captions && current && (\n        <div\n          className=\"pointer-events-none absolute inset-x-0 bottom-[22px] flex flex-col items-center gap-1.5 px-4 text-center animate-[circular-carousel-reveal_900ms_ease_both] motion-reduce:animate-none\"\n          aria-hidden=\"true\"\n        >\n          <span\n            key={active}\n            className=\"flex max-w-full flex-col items-center text-[15px] font-medium leading-[1.35] animate-[circular-carousel-title_520ms_cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none\"\n          >\n            {current.title || current.alt}\n            {current.subtitle && <span className=\"font-normal opacity-60\">{current.subtitle}</span>}\n          </span>\n          <span className=\"inline-flex items-center gap-[3px] text-[12px] leading-none tabular-nums opacity-50\">\n            <Digits value={active + 1} />\n            <span className=\"opacity-60\">/</span>\n            <span>{String(count).padStart(2, '0')}</span>\n          </span>\n        </div>\n      )}\n      <style>{STYLE}</style>\n      <div className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {`${label}, ${active + 1} of ${count}`}\n      </div>\n    </div>\n  );\n};\n\nexport default CircularCarousel;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}