{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slipstream",
  "type": "registry:ui",
  "title": "Slipstream",
  "description": "A spring-smoothed cursor follower: a soft bead of light that stretches into a teardrop at speed, brightening to white in motion and settling neutral at rest. Scoped to its container by default, opt into page-wide with `global`, never follows coarse pointers, scales up over [data-cursor-target] elements, and idles on its own when untouched.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-visibility-pause.json"
  ],
  "files": [
    {
      "path": "registry/default/cursors/slipstream/slipstream.tsx",
      "type": "registry:ui",
      "target": "components/crucible/slipstream.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useMotionValue, useMotionTemplate, useSpring, useTransform, useVelocity } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface SlipstreamProps {\n  className?: string;\n  /** Resting diameter of the follower bead in px. Default 26. */\n  size?: number;\n  /** Scale multiplier while hovering an element carrying `data-cursor-target`. Default 2.2. */\n  targetScale?: number;\n  /** `[rest, peak]` colors — the bead settles to `rest` and brightens toward `peak` with speed. */\n  colors?: [string, string];\n  /** Spring physics for the chase motion. */\n  spring?: { stiffness?: number; damping?: number; mass?: number };\n  /**\n   * Attach to the whole viewport instead of the nearest containing element.\n   * Off by default — Slipstream is scoped to its parent (place it inside a\n   * `relative` container, like Crucible's backgrounds) so it never hijacks\n   * the cursor page-wide unless you explicitly opt in.\n   */\n  global?: boolean;\n  /** Hide the native system cursor while active. Default true (\"replace\" vs. \"augment\"). */\n  hideNativeCursor?: boolean;\n  /**\n   * Idle autoplay (scoped mode only): after 1.5s with no pointer in the\n   * container, a ghost bead swoops a seeded Lissajous path — speeding up and\n   * easing off so the teardrop stretch shows — and swells over\n   * `data-cursor-target` elements it crosses. A real pointer takes over\n   * instantly. Also runs on touch-only devices so the surface never looks\n   * dead. Never under reduced motion; sleeps offscreen. Default true.\n   */\n  idle?: boolean;\n}\n\nconst DEFAULT_SPRING = { stiffness: 380, damping: 32, mass: 0.6 };\nconst IDLE_MS = 1500;\nconst GHOST_SPEED = 360; // mean px/s; modulated so the stretch breathes\n\n/**\n * Slipstream — a spring-smoothed cursor follower: a gradient-cored bead of\n * light (hot white center, soft haloed edge) that stretches into a teardrop\n * along the direction of travel at speed — brightening to its peak color in\n * motion, trailed by a lagging after-glow — and settles to a quiet\n * cyan-tinted neutral at rest. Scoped to its nearest containing element by\n * default (`global` opts into page-wide), never follows coarse/touch\n * pointers (`matchMedia('(pointer: fine)')`), and scales up over any element\n * carrying `data-cursor-target`. With no pointer for 1.5s (scoped mode,\n * `idle`), a ghost bead swoops a Lissajous path so the surface never looks\n * dead — on touch devices too — until a real pointer takes over. It fully\n * disables itself (restoring the native cursor) under\n * `prefers-reduced-motion`.\n */\nexport function Slipstream({\n  className,\n  size = 26,\n  targetScale = 2.2,\n  colors = [\"#bae6fd\", \"#ffffff\"],\n  spring,\n  global = false,\n  hideNativeCursor = true,\n  idle = true,\n}: SlipstreamProps) {\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\n  const reducedMotion = useReducedMotion();\n  const [finePointer, setFinePointer] = React.useState(false);\n  const [hovering, setHovering] = React.useState(false);\n  const [active, setActive] = React.useState(false);\n\n  React.useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mql = window.matchMedia(\"(pointer: fine)\");\n    const sync = () => setFinePointer(mql.matches);\n    sync();\n    mql.addEventListener(\"change\", sync);\n    return () => mql.removeEventListener(\"change\", sync);\n  }, []);\n\n  // Auto-disabled on coarse pointers and under reduced motion — this is the\n  // one Crucible component that fully turns off rather than falling back to\n  // a static frame, per the cursor category's accessibility contract.\n  // The idle ghost (scoped only) also keeps it alive on touch-only devices;\n  // there the real touch never replaces a cursor, it only interrupts the ghost.\n  const ghostAllowed = idle && !global && !reducedMotion;\n  const enabled = (finePointer || ghostAllowed) && !reducedMotion;\n  const [ghosting, setGhosting] = React.useState(false);\n\n  const springCfg = { ...DEFAULT_SPRING, ...spring };\n  const mx = useMotionValue(0);\n  const my = useMotionValue(0);\n  const x = useSpring(mx, springCfg);\n  const y = useSpring(my, springCfg);\n\n  const vx = useVelocity(x);\n  const vy = useVelocity(y);\n  const speed = useTransform([vx, vy], (latest) => {\n    const [lvx, lvy] = latest as number[];\n    return Math.min(Math.hypot(lvx, lvy) / 900, 1);\n  });\n  const angle = useTransform([vx, vy], (latest) => {\n    const [lvx, lvy] = latest as number[];\n    return Math.hypot(lvx, lvy) > 40 ? (Math.atan2(lvy, lvx) * 180) / Math.PI : 0;\n  });\n  // Velocity stretch is deliberately pronounced — the teardrop elongation IS\n  // the component's signature, so it must read at a glance.\n  const stretch = useTransform(speed, (s) => 1 + s * 1.5);\n  const squash = useTransform(speed, (s) => 1 - s * 0.45);\n  const heatPercent = useTransform(speed, (s) => `${Math.round(s * 100)}%`);\n  const bodyColor = useMotionTemplate`color-mix(in oklab, ${colors[1]} ${heatPercent}, ${colors[0]})`;\n  // Gradient core: a hot white center falling off through the speed-mixed body\n  // color — presence at rest, incandescent in motion.\n  const coreBackground = useMotionTemplate`radial-gradient(circle at 50% 45%, #ffffff 0%, ${bodyColor} 38%, transparent 72%)`;\n  const haloOpacity = useTransform(speed, (s) => 0.4 + s * 0.5);\n\n  // Trailing after-glow: a second, softer bead chasing the main one on a much\n  // looser spring, fading in with speed and vanishing at rest.\n  const trailX = useSpring(x, {\n    stiffness: springCfg.stiffness * 0.35,\n    damping: springCfg.damping * 0.9,\n    mass: springCfg.mass * 1.4,\n  });\n  const trailY = useSpring(y, {\n    stiffness: springCfg.stiffness * 0.35,\n    damping: springCfg.damping * 0.9,\n    mass: springCfg.mass * 1.4,\n  });\n  const trailOpacity = useTransform(speed, (s) => Math.min(s * 0.9, 0.5));\n\n  const hoverScale = useSpring(1, { stiffness: 300, damping: 24 });\n  React.useEffect(() => {\n    hoverScale.set(hovering ? targetScale : 1);\n  }, [hovering, targetScale, hoverScale]);\n\n  // Real-pointer presence for the ghost: inside the scope, and when last seen.\n  const presenceRef = React.useRef({ inside: false, lastReal: 0 });\n  // The ghost loop reads the overlay through its own ref (set in the effect),\n  // so the hook never captures `wrapperRef` itself.\n  const overlayRef = React.useRef<HTMLDivElement | null>(null);\n  const ghostRef = React.useRef({ t: 0, last: 0, on: false, frame: 0 });\n\n  const tick = React.useCallback(\n    (elapsed: number) => {\n      const el = overlayRef.current;\n      const g = ghostRef.current;\n      if (elapsed < g.last) g.last = elapsed;\n      const dt = Math.min(elapsed - g.last, 0.05);\n      g.last = elapsed;\n      if (!el) return;\n      const pr = presenceRef.current;\n      if (pr.inside || performance.now() - pr.lastReal < IDLE_MS) {\n        if (g.on) {\n          g.on = false;\n          setGhosting(false);\n        }\n        return;\n      }\n      const w = el.clientWidth;\n      const h = el.clientHeight;\n      if (w < 2 || h < 2) return;\n      const ax = w * 0.38;\n      const ay = h * 0.3;\n      // Speed swells and ebbs so the bead visibly stretches, then relaxes.\n      const surge = 0.55 + 0.75 * Math.pow(0.5 + 0.5 * Math.sin(elapsed * 0.9), 2);\n      g.t += (dt * GHOST_SPEED * surge) / Math.max(100, (ax * 2 + ay * 3) * 0.7);\n      const px = w / 2 + ax * Math.sin(2 * g.t + 1.1);\n      const py = h / 2 + ay * Math.sin(3 * g.t);\n      if (!g.on) {\n        g.on = true;\n        // Seat (no swoop from wherever the real pointer left it).\n        x.jump(px);\n        y.jump(py);\n        trailX.jump(px);\n        trailY.jump(py);\n        setGhosting(true);\n      }\n      mx.set(px);\n      my.set(py);\n      // Swell over marked targets the ghost crosses (hit-test ~10x/s).\n      if (++g.frame % 6 === 0) {\n        const rect = el.getBoundingClientRect();\n        const hit = document.elementFromPoint(rect.left + px, rect.top + py);\n        const parent = el.parentElement;\n        const tgt = hit?.closest(\"[data-cursor-target]\");\n        setHovering(!!tgt && !!parent && parent.contains(tgt));\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    []\n  );\n\n  // The ghost loop rides the inner layer so it sleeps offscreen / on hidden tabs.\n  const ghostLoopRef = useVisibilityPause<HTMLDivElement>(tick, { paused: !enabled || !ghostAllowed });\n\n  React.useEffect(() => {\n    if (!enabled) return;\n    const target: Window | HTMLElement | null = global ? window : (wrapperRef.current?.parentElement ?? null);\n    if (!target) return;\n    const cursorRoot: HTMLElement = global ? document.documentElement : (target as HTMLElement);\n    const pr = presenceRef.current;\n    overlayRef.current = wrapperRef.current;\n    pr.lastReal = performance.now();\n\n    const onMove = (e: PointerEvent) => {\n      pr.inside = true;\n      pr.lastReal = performance.now();\n      // Touch-only devices: a touch interrupts the ghost but draws no bead.\n      if (!finePointer) {\n        setActive(false);\n        return;\n      }\n      let px = e.clientX;\n      let py = e.clientY;\n      if (!global) {\n        const rect = (target as HTMLElement).getBoundingClientRect();\n        px -= rect.left;\n        py -= rect.top;\n      }\n      mx.set(px);\n      my.set(py);\n      setActive(true);\n      setHovering(!!(e.target as Element | null)?.closest(\"[data-cursor-target]\"));\n    };\n    const onLeave = () => {\n      pr.inside = false;\n      pr.lastReal = performance.now();\n      setActive(false);\n      setHovering(false);\n    };\n\n    target.addEventListener(\"pointermove\", onMove as EventListener);\n    if (!global) {\n      target.addEventListener(\"pointerleave\", onLeave as EventListener);\n      target.addEventListener(\"pointercancel\", onLeave as EventListener);\n    }\n    if (hideNativeCursor && finePointer) cursorRoot.style.cursor = \"none\";\n\n    return () => {\n      target.removeEventListener(\"pointermove\", onMove as EventListener);\n      if (!global) {\n        target.removeEventListener(\"pointerleave\", onLeave as EventListener);\n        target.removeEventListener(\"pointercancel\", onLeave as EventListener);\n      }\n      if (hideNativeCursor && finePointer) cursorRoot.style.cursor = \"\";\n      pr.inside = false;\n    };\n  }, [enabled, finePointer, global, hideNativeCursor, mx, my]);\n\n  if (!enabled) return null;\n\n  return (\n    <div\n      ref={wrapperRef}\n      aria-hidden\n      data-crucible=\"slipstream\"\n      className={cn(\n        \"pointer-events-none\",\n        global ? \"fixed inset-0 z-[999]\" : \"absolute inset-0 z-50 overflow-hidden\",\n        className\n      )}\n    >\n      <div\n        ref={ghostLoopRef}\n        className=\"absolute inset-0\"\n        // The ghost reads as a deliberate stand-in: slightly quieter than a real pointer.\n        style={{ opacity: active ? 1 : ghosting ? 0.8 : 0, transition: \"opacity 300ms ease\" }}\n      >\n        {/* Trailing after-glow — lags the main bead, only visible in motion. */}\n        <motion.div\n          className=\"absolute top-0 left-0 rounded-full\"\n          style={{\n            x: trailX,\n            y: trailY,\n            translateX: \"-50%\",\n            translateY: \"-50%\",\n            width: size * 0.9,\n            height: size * 0.9,\n            rotate: angle,\n            scaleX: stretch,\n            scaleY: squash,\n            opacity: trailOpacity,\n            background: `radial-gradient(circle, ${colors[1]} 0%, transparent 70%)`,\n            filter: \"blur(5px)\",\n          }}\n        />\n        {/* Main bead: soft halo + gradient core, stretched along travel. */}\n        <motion.div\n          className=\"absolute top-0 left-0\"\n          style={{\n            x,\n            y,\n            translateX: \"-50%\",\n            translateY: \"-50%\",\n            width: size,\n            height: size,\n            rotate: angle,\n            scaleX: stretch,\n            scaleY: squash,\n            scale: hoverScale,\n          }}\n        >\n          <motion.div\n            className=\"absolute rounded-full\"\n            style={{\n              inset: -size * 0.55,\n              background: `radial-gradient(circle, ${colors[0]} 0%, transparent 70%)`,\n              opacity: haloOpacity,\n              filter: \"blur(6px)\",\n            }}\n          />\n          <motion.div\n            className=\"absolute inset-0 rounded-full\"\n            style={{ background: coreBackground, filter: \"blur(0.4px)\" }}\n          />\n        </motion.div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}