{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"after-cursor","type":"registry:ui","title":"After Cursor","description":"Translucent, drifting echoes of the cursor trail behind the pointer like afterimages.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/after-cursor.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst SHAPES = {\n  arrow: {\n    hot: [3, 2],\n    body: <path d=\"M3 2v18.5l5.2-4.7 3.4 7.2 3.2-1.5-3.3-7h7.2z\" strokeWidth=\"1.3\" strokeLinejoin=\"round\" />,\n  },\n  ghost: {\n    hot: [12, 12],\n    body: (\n      <>\n        <path d=\"M12 2.5a8 8 0 0 0-8 8v11l2.7-2 2.6 2 2.7-2 2.6 2 2.7-2 2.7 2v-11a8 8 0 0 0-8-8z\" strokeWidth=\"1.2\" />\n        <circle cx=\"9.3\" cy=\"10.5\" r=\"1.4\" fill=\"#0a0a0a\" stroke=\"none\" />\n        <circle cx=\"14.7\" cy=\"10.5\" r=\"1.4\" fill=\"#0a0a0a\" stroke=\"none\" />\n      </>\n    ),\n  },\n  dot: { hot: [12, 12], body: <circle cx=\"12\" cy=\"12\" r=\"6\" strokeWidth=\"1.2\" /> },\n};\n\nexport interface AfterCursorProps {\n  children?: ReactNode;\n  /** Color of the live cursor. */\n  color?: string;\n  /** Tint of the trailing ghosts. */\n  ghostColor?: string;\n  /** Number of ghost copies. */\n  count?: number;\n  /** Frames of delay between consecutive ghosts. */\n  spacing?: number;\n  /** Cursor size in px. */\n  size?: number;\n  shape?: keyof typeof SHAPES;\n  /** Hide the native cursor inside the container. */\n  hideCursor?: boolean;\n  className?: string;\n}\n\nexport function AfterCursor({\n  children,\n  color = \"#ffffff\",\n  ghostColor = \"#a78bfa\",\n  count = 6,\n  spacing = 4,\n  size = 28,\n  shape = \"arrow\",\n  hideCursor = true,\n  className,\n}: AfterCursorProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const layerRef = useRef<HTMLDivElement>(null);\n  const { hot, body } = SHAPES[shape] ?? SHAPES.arrow;\n\n  useEffect(() => {\n    const root = rootRef.current!;\n    const layer = layerRef.current!;\n    const els = Array.from(layer.children) as HTMLElement[];\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const len = count * spacing + 1;\n    const hx = (hot[0] / 24) * size;\n    const hy = (hot[1] / 24) * size;\n    let hist: { x: number; y: number }[] = [];\n    const target = { x: 0, y: 0 };\n    let inside = false;\n    let raf = 0;\n\n    const loop = (now: number) => {\n      hist.unshift({ ...target });\n      if (hist.length > len) hist.length = len;\n      const t = now / 1000;\n      // Index 0 is the live cursor; ghosts read older positions and drift a little, like smoke.\n      els.forEach((el, i) => {\n        const p = hist[Math.min(hist.length - 1, (count - i) * spacing)];\n        const g = count - i;\n        const drift = reduced ? 0 : g * 1.6;\n        const x = p.x - hx + Math.sin(t * 2.1 + g) * drift;\n        const y = p.y - hy + Math.cos(t * 1.7 + g * 1.3) * drift - (reduced ? 0 : g * 2.2);\n        el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${1 - g * 0.035})`;\n      });\n      const settled = hist.every((p) => p.x === target.x && p.y === target.y);\n      raf = inside || !settled ? requestAnimationFrame(loop) : 0;\n    };\n\n    const onMove = (e: PointerEvent) => {\n      const r = root.getBoundingClientRect();\n      target.x = e.clientX - r.left;\n      target.y = e.clientY - r.top;\n      if (!inside) {\n        inside = true;\n        layer.style.opacity = \"1\";\n        if (!hist.length || reduced) hist = [{ ...target }];\n      }\n      if (reduced) hist = [{ ...target }];\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n    const onLeave = () => {\n      inside = false;\n      layer.style.opacity = \"0\";\n    };\n\n    root.addEventListener(\"pointermove\", onMove, { passive: true });\n    root.addEventListener(\"pointerleave\", onLeave);\n    return () => {\n      cancelAnimationFrame(raf);\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n    };\n  }, [count, spacing, size, hot]);\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\"relative overflow-hidden\", className)}\n      data-ghost-cursor-hide={hideCursor ? \"\" : undefined}\n    >\n      <style href=\"after-cursor\" precedence=\"default\">\n        {`[data-ghost-cursor-hide],[data-ghost-cursor-hide] *{cursor:none!important}`}\n      </style>\n      {children}\n      <div\n        ref={layerRef}\n        aria-hidden\n        style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\", opacity: 0, transition: \"opacity .3s\" }}\n      >\n        {Array.from({ length: count + 1 }, (_, i) => {\n          const g = count - i;\n          const live = g === 0;\n          return (\n            <svg\n              key={i}\n              viewBox=\"0 0 24 24\"\n              width={size}\n              height={size}\n              fill={live ? color : ghostColor}\n              stroke={live ? \"#000\" : ghostColor}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                willChange: \"transform\",\n                opacity: live ? 1 : 0.75 * (1 - g / (count + 1)),\n                filter: live ? \"drop-shadow(0 2px 4px rgba(0,0,0,.5))\" : `blur(${g * 0.5}px)`,\n                mixBlendMode: live ? undefined : \"screen\",\n              }}\n            >\n              {body}\n            </svg>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"}]}