{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel-3d",
  "title": "Carousel 3D",
  "author": "AlgorizeTech",
  "description": "A perspective carousel where cards rotate through depth, with drag and autoplay. Use it for portfolio and gallery sections that need presence.",
  "dependencies": [
    "gsap",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ThreeDCarouselBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport gsap from \"gsap\";\nimport { ChevronLeft, ChevronRight, Pause, Play } from \"lucide-react\";\nimport React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { bezierEase, gsapTicker } from \"./gsap-motion\";\nimport { createSpringFollower, MOTION_EASE, springEase } from \"./motion-curves\";\n\nexport interface CarouselItem {\n  id: string;\n  title: string;\n  category: string;\n  image: string;\n  description?: string;\n}\n\nexport interface ThreeDCarouselProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  items: CarouselItem[];\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  /** Renders the previous / next and autoplay pause buttons. Defaults to `true`. */\n  showArrows?: boolean;\n  /** Renders the pagination dots under the deck. Defaults to `true`. */\n  showDots?: boolean;\n}\n\n// ------------------------------------------\n// Motion — the same numbers as the motion/ build\n// ------------------------------------------\n\n/** Fan slot of the outer card, written as one transform in Motion's order. */\ntype Slot = { x: number; z: number; scale: number; rotateY: number; zIndex: number; opacity: number };\ntype SlotKey = keyof Slot;\n\n/** Fanning between slots. */\nconst SLOT_SPRING = { stiffness: 280, damping: 28 };\n/** Hover tilt of the inner face. */\nconst TILT_SPRING = { stiffness: 300, damping: 25 };\n/** The spring that pulls a released card back inside its 0/0 drag constraints. */\nconst BOUNCE_SPRING = { stiffness: 200, damping: 40 };\n/** Motion's default spring for a scale returning to 1 once hover ends. */\nconst HOVER_OUT_SPRING = { stiffness: 550, damping: 30 };\n/** Hover in: a 0.2s tween on Motion's default `easeOut`. */\nconst HOVER_EASE = bezierEase(MOTION_EASE.easeOut);\nconst DRAG_ELASTIC = 0.2;\n\n/** Drag styles Motion puts on an element while `drag=\"x\"` is on. */\nconst DRAG_STYLE = {\n  userSelect: \"none\",\n  WebkitUserSelect: \"none\",\n  WebkitTouchCallout: \"none\",\n  touchAction: \"pan-y\",\n} as React.CSSProperties;\n\n/** While a card is being dragged, hover starts and ends are ignored (as in Motion). */\nlet dragActive = false;\n\ninterface Run {\n  tween: gsap.core.Tween;\n  from: number;\n  to: number;\n  duration: number;\n  ease: (progress: number) => number;\n}\n\n/** Speed of a running animation right now, so a new target keeps its momentum. */\nfunction velocityOf(run: Run | undefined) {\n  if (!run) return 0;\n  const p = run.tween.progress();\n  if (p >= 1) return 0;\n  const a = Math.max(0, p - 0.001);\n  const b = Math.min(1, p + 0.001);\n  return (((run.ease(b) - run.ease(a)) / (b - a)) * (run.to - run.from)) / run.duration;\n}\n\n/** Spring (or tween) one numeric property of `state`, carrying over the velocity of the last run. */\nfunction animateKey<T extends Record<string, number>>(\n  state: T,\n  runs: Partial<Record<keyof T, Run>>,\n  key: keyof T,\n  to: number,\n  render: () => void,\n  tween?: { duration: number; ease: (progress: number) => number },\n  spring: { stiffness: number; damping: number } = SLOT_SPRING,\n) {\n  const velocity = velocityOf(runs[key]);\n  runs[key]?.tween.kill();\n  delete runs[key];\n  const from = state[key];\n  if (from === to) return;\n  const { duration, ease } =\n    tween ?? springEase({ ...spring, velocity: velocity / (to - from) });\n  const run = gsap.to(state, { [key]: to, duration, ease, onUpdate: render });\n  runs[key] = { tween: run, from, to, duration, ease };\n}\n\nfunction outerTransform(s: Slot) {\n  let t = \"\";\n  if (s.x) t += `translateX(${s.x}px) `;\n  if (s.z) t += `translateZ(${s.z}px) `;\n  if (s.scale !== 1) t += `scale(${s.scale}) `;\n  if (s.rotateY) t += `rotateY(${s.rotateY}deg) `;\n  return t ? t.trim() : \"none\";\n}\n\nfunction innerTransform(scale: number, rotateX: number, rotateY: number) {\n  let t = \"\";\n  if (scale !== 1) t += `scale(${scale}) `;\n  if (rotateX) t += `rotateX(${rotateX}deg) `;\n  if (rotateY) t += `rotateY(${rotateY}deg) `;\n  return t ? t.trim() : \"none\";\n}\n\nfunction slotFor(offset: number, spacing: number): Slot {\n  const isCenter = offset === 0;\n  return {\n    x: offset * spacing,\n    scale: isCenter ? 1 : Math.max(0.7, 1 - Math.abs(offset) * 0.15),\n    rotateY: offset * -22,\n    z: -Math.abs(offset) * 140,\n    zIndex: 30 - Math.abs(offset) * 10,\n    opacity: isCenter ? 1 : Math.max(0.3, 1 - Math.abs(offset) * 0.35),\n  };\n}\n\n/** Card spacing: tighter under 640px. Read on the client only. */\nconst spacingNow = () => (window.innerWidth < 640 ? 120 : 220);\n\n/** Tilt range: pointer offset ±150px maps onto ±14deg, clamped. */\nconst tiltFrom = (offset: number) =>\n  Math.max(-14, Math.min(14, (offset / 150) * 14));\n\n// Sub-component for individual cards to cleanly manage 3D tilt without loop issues\ninterface CarouselCardProps {\n  item: CarouselItem;\n  index: number;\n  activeIndex: number;\n  total: number;\n  onClick: () => void;\n  onDragEnd: (offsetX: number) => void;\n}\n\nfunction CarouselCard({\n  item,\n  index,\n  activeIndex,\n  total,\n  onClick,\n  onDragEnd,\n}: CarouselCardProps) {\n  // Calculate circular distance offset for fanned 3D layout\n  let diff = (index - activeIndex) % total;\n  if (diff > Math.floor(total / 2)) diff -= total;\n  if (diff < -Math.floor(total / 2)) diff += total;\n\n  const offset = diff;\n  const isCenter = offset === 0;\n  const hidden = Math.abs(offset) > 2;\n\n  const outerRef = useRef<HTMLDivElement>(null);\n  const innerRef = useRef<HTMLDivElement>(null);\n\n  // The server has no window, so markup always starts from the desktop\n  // spacing; the real spacing is applied before the first paint.\n  const [serverStyle] = useState(() => {\n    const slot = slotFor(offset, 220);\n    return { transform: outerTransform(slot), zIndex: slot.zIndex, opacity: slot.opacity };\n  });\n\n  const outer = useRef({\n    el: null as HTMLDivElement | null,\n    slot: slotFor(offset, 220),\n    targets: slotFor(offset, 220),\n    runs: {} as Partial<Record<SlotKey, Run>>,\n  });\n  const inner = useRef({\n    el: null as HTMLDivElement | null,\n    pose: { scale: 1, rotateX: 0, rotateY: 0 },\n    runs: {} as Partial<Record<\"scale\", Run>>,\n    hovered: false,\n    hoverScale: 1,\n  });\n  const tilt = useRef<{ set: (x: number, y: number) => void } | null>(null);\n  const endDrag = useRef<(() => void) | null>(null);\n  const dragEnd = useRef(onDragEnd);\n  // Latest callback for the drag release; synced after commit, never in render.\n  useLayoutEffect(() => {\n    dragEnd.current = onDragEnd;\n  });\n\n  const renderOuter = () => {\n    const { el, slot } = outer.current;\n    if (!el) return;\n    el.style.transform = outerTransform(slot);\n    el.style.zIndex = String(Math.round(slot.zIndex));\n    el.style.opacity = String(slot.opacity);\n  };\n  const renderInner = () => {\n    const { el, pose } = inner.current;\n    if (el) el.style.transform = innerTransform(pose.scale, pose.rotateX, pose.rotateY);\n  };\n\n  // 3D Tilt springs for interactive hover; they outlive the card's DOM while\n  // it is out of the fan, exactly like the hooks they replace.\n  useLayoutEffect(() => {\n    const rotateX = createSpringFollower(\n      0,\n      (v) => {\n        inner.current.pose.rotateX = v;\n        renderInner();\n      },\n      { ...TILT_SPRING, ticker: gsapTicker },\n    );\n    const rotateY = createSpringFollower(\n      0,\n      (v) => {\n        inner.current.pose.rotateY = v;\n        renderInner();\n      },\n      { ...TILT_SPRING, ticker: gsapTicker },\n    );\n    tilt.current = {\n      set: (x, y) => {\n        rotateX.set(-tiltFrom(y));\n        rotateY.set(tiltFrom(x));\n      },\n    };\n    const outerMotion = outer.current;\n    const innerMotion = inner.current;\n    return () => {\n      rotateX.destroy();\n      rotateY.destroy();\n      tilt.current = null;\n      endDrag.current?.();\n      Object.values(outerMotion.runs).forEach((run) => run?.tween.kill());\n      outerMotion.runs = {};\n      outerMotion.el = null;\n      innerMotion.runs.scale?.tween.kill();\n      innerMotion.runs = {};\n      innerMotion.el = null;\n    };\n  }, []);\n\n  // Fan into the slot for this offset on every render (initial={false}: a\n  // card entering the fan appears in place).\n  useLayoutEffect(() => {\n    const o = outer.current;\n    const i = inner.current;\n    const el = outerRef.current;\n    if (!el) {\n      if (o.el) {\n        Object.values(o.runs).forEach((run) => run?.tween.kill());\n        o.runs = {};\n        o.el = null;\n        i.runs.scale?.tween.kill();\n        i.runs = {};\n        i.el = null;\n      }\n      return;\n    }\n    const target = slotFor(offset, spacingNow());\n    if (o.el !== el) {\n      o.el = el;\n      o.slot = { ...target };\n      o.targets = { ...target };\n      renderOuter();\n      // A fresh face: no hover yet, but the tilt springs carry on.\n      i.el = innerRef.current;\n      i.pose.scale = 1;\n      i.hovered = false;\n      renderInner();\n      return;\n    }\n    (Object.keys(target) as SlotKey[]).forEach((key) => {\n      if (o.targets[key] === target[key]) return;\n      o.targets[key] = target[key];\n      animateKey(o.slot, o.runs, key, target[key], renderOuter);\n    });\n    // Hovered while the card changes role: move to the new hover scale.\n    const hoverScale = isCenter ? 1.02 : 1.05;\n    if (i.hovered && i.hoverScale !== hoverScale) {\n      i.hoverScale = hoverScale;\n      animateKey(i.pose, i.runs, \"scale\", hoverScale, renderInner, { duration: 0.2, ease: HOVER_EASE });\n    }\n  });\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect();\n    const centerX = rect.left + rect.width / 2;\n    const centerY = rect.top + rect.height / 2;\n    tilt.current?.set(e.clientX - centerX, e.clientY - centerY);\n  };\n\n  const handleMouseLeave = () => {\n    tilt.current?.set(0, 0);\n  };\n\n  const handlePointerEnter = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType === \"touch\" || dragActive) return;\n    const i = inner.current;\n    i.hovered = true;\n    i.hoverScale = isCenter ? 1.02 : 1.05;\n    animateKey(i.pose, i.runs, \"scale\", i.hoverScale, renderInner, { duration: 0.2, ease: HOVER_EASE });\n  };\n\n  const handlePointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {\n    const i = inner.current;\n    if (e.pointerType === \"touch\" || dragActive || !i.hovered) return;\n    i.hovered = false;\n    animateKey(i.pose, i.runs, \"scale\", 1, renderInner, undefined, HOVER_OUT_SPRING);\n  };\n\n  // Horizontal drag of the centre card: 0/0 constraints with 0.2 elastic, so\n  // it trails the pointer at 0.2x and springs home on release.\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType === \"mouse\" ? e.button > 0 : e.isPrimary === false) return;\n    const o = outer.current;\n    endDrag.current?.();\n    // Grabbing the card stops whatever its x was doing.\n    o.runs.x?.tween.kill();\n    delete o.runs.x;\n\n    const startX = e.pageX;\n    const startY = e.pageY;\n    let lastX = startX;\n    let originX = 0;\n    let dragging = false;\n\n    const move = (ev: PointerEvent) => {\n      lastX = ev.pageX;\n      const offsetX = ev.pageX - startX;\n      if (!dragging) {\n        if (Math.hypot(offsetX, ev.pageY - startY) < 3) return;\n        dragging = true;\n        dragActive = true;\n        originX = o.slot.x;\n      }\n      o.slot.x = (originX + offsetX) * DRAG_ELASTIC;\n      renderOuter();\n    };\n    const up = (ev: PointerEvent) => {\n      remove();\n      if (!dragging) return;\n      const offsetX = (ev.type === \"pointercancel\" ? lastX : ev.pageX) - startX;\n      animateKey(o.slot, o.runs, \"x\", 0, renderOuter, undefined, BOUNCE_SPRING);\n      requestAnimationFrame(() => dragEnd.current(offsetX));\n    };\n    const remove = () => {\n      window.removeEventListener(\"pointermove\", move);\n      window.removeEventListener(\"pointerup\", up);\n      window.removeEventListener(\"pointercancel\", up);\n      if (dragging) dragActive = false;\n      endDrag.current = null;\n    };\n    window.addEventListener(\"pointermove\", move);\n    window.addEventListener(\"pointerup\", up);\n    window.addEventListener(\"pointercancel\", up);\n    endDrag.current = remove;\n  };\n\n  // Do not render cards that are too far out of the fan view\n  if (hidden) return null;\n\n  return (\n    <div\n      ref={outerRef}\n      onClick={onClick}\n      onPointerDown={isCenter ? handlePointerDown : undefined}\n      draggable={isCenter ? false : undefined}\n      style={isCenter ? { ...serverStyle, ...DRAG_STYLE } : serverStyle}\n      className={cn(\n        \"absolute w-[280px] sm:w-[320px] md:w-[360px] h-[360px] sm:h-[400px] md:h-[420px] rounded-3xl\",\n        isCenter\n          ? \"cursor-grab active:cursor-grabbing z-30\"\n          : \"cursor-pointer z-10\",\n      )}\n    >\n      {/* Inner wrapper separates hover tilt transforms from outer fanned layout transforms */}\n      <div\n        ref={innerRef}\n        onMouseMove={handleMouseMove}\n        onMouseLeave={handleMouseLeave}\n        onPointerEnter={handlePointerEnter}\n        onPointerLeave={handlePointerLeave}\n        className={cn(\n          \"w-full h-full rounded-3xl overflow-hidden shadow-2xl transition-shadow duration-300\",\n          \"bg-card border border-border\",\n          isCenter\n            ? \"shadow-primary/20 dark:shadow-primary/20 ring-2 ring-primary/30\"\n            : \"hover:border-primary/50 shadow-md\",\n        )}\n      >\n        {/* Image Background */}\n        <div className=\"relative w-full h-full overflow-hidden\">\n          <img\n            src={item.image}\n            alt={item.title}\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"w-full h-full object-cover pointer-events-none transition-transform duration-700 hover:scale-105\"\n          />\n\n          {/* Theme Adaptive Overlay */}\n          <div className=\"absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent dark:from-black/95 dark:via-black/40 dark:to-transparent\" />\n\n          {/* Content Payload */}\n          <div className=\"absolute inset-x-0 bottom-0 p-6 flex flex-col justify-end pointer-events-none text-white\">\n            <span className=\"text-xs font-semibold uppercase tracking-wider px-3 py-1 rounded-full bg-primary/90 backdrop-blur-md w-fit mb-2 text-primary-foreground shadow-sm\">\n              {item.category}\n            </span>\n            <h3 className=\"text-xl md:text-2xl font-semibold tracking-tight mb-1 leading-snug\">\n              {item.title}\n            </h3>\n            {item.description && (\n              <p className=\"text-sm text-zinc-300 dark:text-zinc-400 line-clamp-2 leading-relaxed\">\n                {item.description}\n              </p>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport const ThreeDCarousel = React.forwardRef<\n  HTMLDivElement,\n  ThreeDCarouselProps\n>(\n  (\n    {\n      items,\n      autoPlay = true,\n      autoPlayInterval = 4000,\n      showArrows = true,\n      showDots = true,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const [activeIndex, setActiveIndex] = useState(0);\n    const [isPlaying, setIsPlaying] = useState(autoPlay);\n    const [isHovered, setIsHovered] = useState(false);\n    const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n    const total = items.length;\n\n    const handleNext = useCallback(() => {\n      setActiveIndex((prev) => (prev + 1) % total);\n    }, [total]);\n\n    const handlePrev = useCallback(() => {\n      setActiveIndex((prev) => (prev - 1 + total) % total);\n    }, [total]);\n\n    // Keyboard Navigation\n    useEffect(() => {\n      const handleKeyDown = (e: KeyboardEvent) => {\n        if (e.key === \"ArrowLeft\") handlePrev();\n        if (e.key === \"ArrowRight\") handleNext();\n      };\n      window.addEventListener(\"keydown\", handleKeyDown);\n      return () => window.removeEventListener(\"keydown\", handleKeyDown);\n    }, [handleNext, handlePrev]);\n\n    // Auto-Play Timer\n    useEffect(() => {\n      if (isPlaying && !isHovered) {\n        timerRef.current = setInterval(handleNext, autoPlayInterval);\n      } else if (timerRef.current) {\n        clearInterval(timerRef.current);\n      }\n      return () => {\n        if (timerRef.current) clearInterval(timerRef.current);\n      };\n    }, [isPlaying, isHovered, handleNext, autoPlayInterval]);\n\n    // Drag / Swipe Interaction\n    const handleDragEnd = (offsetX: number) => {\n      const threshold = 50;\n      if (offsetX < -threshold) {\n        handleNext();\n      } else if (offsetX > threshold) {\n        handlePrev();\n      }\n    };\n\n    return (\n      <div\n        ref={ref}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n        className={cn(\n          \"relative w-full h-[500px] md:h-[580px] flex flex-col items-center justify-center overflow-hidden select-none py-6\",\n          className,\n        )}\n        {...props}\n      >\n        {/* 3D Fanned Cards Container */}\n        <div className=\"relative w-full max-w-4xl h-[360px] md:h-[420px] flex items-center justify-center perspective-[1200px]\">\n          {items.map((item, index) => (\n            <CarouselCard\n              key={item.id}\n              item={item}\n              index={index}\n              activeIndex={activeIndex}\n              total={total}\n              onClick={() => index !== activeIndex && setActiveIndex(index)}\n              onDragEnd={handleDragEnd}\n            />\n          ))}\n        </div>\n\n        {/* Navigation Controls Bar */}\n        {showArrows && (\n          <div className=\"flex items-center gap-4 mt-6 z-10\">\n            <button\n              onClick={handlePrev}\n              aria-label=\"Previous Slide\"\n              className=\"p-3 rounded-full bg-background border border-border text-foreground hover:bg-primary hover:text-primary-foreground hover:border-primary/30 hover:scale-105 active:scale-95 transition-all shadow-md\"\n            >\n              <ChevronLeft className=\"w-5 h-5\" />\n            </button>\n\n            {/* Auto-play Toggle Button */}\n            <button\n              onClick={() => setIsPlaying(!isPlaying)}\n              aria-label={isPlaying ? \"Pause Auto-play\" : \"Start Auto-play\"}\n              className=\"p-3 rounded-full bg-background border border-border text-foreground hover:bg-primary hover:text-primary-foreground hover:border-primary/30 hover:scale-105 active:scale-95 transition-all shadow-md\"\n            >\n              {isPlaying ? (\n                <Pause className=\"w-5 h-5\" />\n              ) : (\n                <Play className=\"w-5 h-5\" />\n              )}\n            </button>\n\n            <button\n              onClick={handleNext}\n              aria-label=\"Next Slide\"\n              className=\"p-3 rounded-full bg-background border border-border text-foreground hover:bg-primary hover:text-primary-foreground hover:border-primary/30 hover:scale-105 active:scale-95 transition-all shadow-md\"\n            >\n              <ChevronRight className=\"w-5 h-5\" />\n            </button>\n          </div>\n        )}\n\n        {/* Pagination Dots */}\n        {showDots && (\n          <div className=\"flex items-center gap-2 mt-4 z-10\">\n            {items.map((_, idx) => (\n              <button\n                key={idx}\n                onClick={() => setActiveIndex(idx)}\n                aria-label={`Go to slide ${idx + 1}`}\n                className={cn(\n                  \"h-1.5 rounded-full transition-all duration-300\",\n                  idx === activeIndex\n                    ? \"w-8 bg-primary\"\n                    : \"w-1.5 bg-muted hover:bg-primary/50\",\n                )}\n              />\n            ))}\n          </div>\n        )}\n      </div>\n    );\n  },\n);\n\nThreeDCarousel.displayName = \"ThreeDCarousel\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/ThreeDCarouselBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/gsap-motion.ts",
      "content": "import gsap from \"gsap\";\nimport { cubicBezier, type FrameTicker } from \"./motion-curves\";\n\n/**\n * GSAP-side glue for the GSAP builds of components that also ship a Motion\n * Motion twin. Kept apart from motion-curves.ts so the library-free curves\n * never pull GSAP into a CSS or Motion build.\n */\n\n/** `gsap.ticker` as a FrameTicker, so spring followers run on GSAP's loop. */\nexport const gsapTicker: FrameTicker = {\n  add(callback) {\n    const tick = (_time: number, deltaTime: number) => callback(deltaTime);\n    gsap.ticker.add(tick);\n    return () => gsap.ticker.remove(tick);\n  },\n};\n\n/** A Motion / CSS cubic-bezier array as a GSAP ease. */\nexport const bezierEase = (curve: readonly [number, number, number, number] | readonly number[]) =>\n  cubicBezier(curve[0], curve[1], curve[2], curve[3]);\n\n/** `(prefers-reduced-motion: reduce)` right now. */\nexport const prefersReducedMotion = () =>\n  typeof window !== \"undefined\" &&\n  window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/gsap-motion.ts"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    }
  ],
  "docs": "Named exports: ThreeDCarousel, CarouselItem. `items` take { id, title, category, image, description? }. `autoPlay` with `autoPlayInterval` advances it; dragging pauses. Renders a plain <img>, so it works outside Next.js.",
  "categories": [
    "media",
    "marketing"
  ],
  "type": "registry:component"
}
