# Comet Cursor Trail on Canvas

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/comet-cursor-trail-drawn-on-one-canvas).

- Page, with a live preview: https://shadcn.exchange/r/weinc/comet-cursor-trail-drawn-on-one-canvas
- Screenshot: https://shadcn.exchange/shot/weinc/comet-cursor-trail-drawn-on-one-canvas/light.png
- Registry item: https://we.inc/r/comet-cursor-trail-drawn-on-one-canvas.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/comet-cursor-trail-drawn-on-one-canvas.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `framer-motion`

## Files

### `components/we-inc/comet-cursor-trail-drawn-on-one-canvas.tsx`

```tsx
import { useEffect, useRef } from "react";
import { useReducedMotion } from "framer-motion";

const LEN = 40; // points in the tail; longer means a longer comet
const WIDTH = 11; // tail thickness at the head, in px
const RGB = "255,232,190"; // tail colour
const dates = [
  { d: "Fri 7 Nov", what: "The Moon and Saturn" },
  { d: "Sat 13 Dec", what: "Watching for meteors" },
  { d: "Fri 9 Jan", what: "The Orion Nebula" },
];
const STARS = [[12, 18, 2], [33, 72, 1.6], [58, 26, 2.2], [76, 64, 1.6], [88, 14, 2], [46, 88, 1.6], [92, 82, 1.6], [22, 48, 2], [68, 44, 1.6], [4, 86, 1.6]]
  .map(([x, y, r]) => `radial-gradient(${r}px ${r}px at ${x}% ${y}%, #fff 50%, transparent 51%)`)
  .join(",");

export default function CometCursorTrailDrawnOnOneCanvas() {
  const reduce = useReducedMotion();
  const rootRef = useRef<HTMLElement>(null);
  const cvRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const root = rootRef.current, cv = cvRef.current;
    if (!root || !cv || reduce || !matchMedia("(hover: hover) and (pointer: fine)").matches) return;
    const ctx = cv.getContext("2d");
    if (!ctx) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const pts: number[] = [];
    let W = 0, H = 0, hx = 0, hy = 0, tx = 0, ty = 0, live = false, frame = 0, idle = 0;
    const t0 = performance.now();
    const size = () => { W = root.clientWidth; H = root.clientHeight; cv.width = W * dpr; cv.height = H * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); };
    const loop = (now: number) => {
      if (!live) {
        const t = (now - t0) / 1000;
        tx = W * (0.55 + 0.4 * Math.cos(t * 1.1)); ty = H * (0.5 + 0.34 * Math.sin(t * 1.65));
      }
      hx += (tx - hx) * 0.22; hy += (ty - hy) * 0.22;
      pts.push(hx, hy);
      if (pts.length > LEN * 2) pts.splice(0, 2);
      ctx.clearRect(0, 0, W, H);
      ctx.lineCap = "round";
      const n = pts.length / 2;
      for (let i = 1; i < n; i++) {
        const k = i / n;
        ctx.strokeStyle = `rgba(${RGB},${k * k * 0.9})`;
        ctx.lineWidth = WIDTH * k;
        ctx.beginPath(); ctx.moveTo(pts[i * 2 - 2], pts[i * 2 - 1]); ctx.lineTo(pts[i * 2], pts[i * 2 + 1]); ctx.stroke();
      }
      const g = ctx.createRadialGradient(hx, hy, 0, hx, hy, 30);
      g.addColorStop(0, "rgba(255,255,255,.95)"); g.addColorStop(0.25, `rgba(${RGB},.5)`); g.addColorStop(1, `rgba(${RGB},0)`);
      ctx.fillStyle = g; ctx.fillRect(hx - 30, hy - 30, 60, 60);
      frame = requestAnimationFrame(loop);
    };
    const start = () => { if (!frame) frame = requestAnimationFrame(loop); };
    const stop = () => { cancelAnimationFrame(frame); frame = 0; };
    const onMove = (e: PointerEvent) => {
      if (e.pointerType === "touch") return;
      const r = root.getBoundingClientRect(); tx = e.clientX - r.left; ty = e.clientY - r.top;
      live = true; clearTimeout(idle); root.removeAttribute("data-kb");
    };
    const onLeave = () => { idle = window.setTimeout(() => { live = false; }, 1500); };
    const onFocus = () => { if (root.querySelector(":focus-visible")) root.setAttribute("data-kb", ""); };
    const onBlur = () => root.removeAttribute("data-kb");
    size();
    const ro = new ResizeObserver(size);
    ro.observe(root);
    const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()));
    io.observe(root);
    root.addEventListener("pointermove", onMove);
    root.addEventListener("pointerleave", onLeave);
    root.addEventListener("focusin", onFocus);
    root.addEventListener("focusout", onBlur);
    return () => {
      stop(); io.disconnect(); ro.disconnect(); clearTimeout(idle);
      ctx.clearRect(0, 0, W, H);
      root.removeEventListener("pointermove", onMove);
      root.removeEventListener("pointerleave", onLeave);
      root.removeEventListener("focusin", onFocus);
      root.removeEventListener("focusout", onBlur);
    };
  }, [reduce]);

  return (
    <section ref={rootRef} className="w2cmt-root relative overflow-hidden px-5 py-14 text-[#eef2fb] sm:px-12 sm:py-[72px]" style={{ background: `${STARS}, radial-gradient(90% 70% at 80% 0%, #1b2d5c 0%, transparent 70%), #0b1630` }}>
      <style>{`
        .w2cmt-cv { transition: opacity .4s ease; }
        .w2cmt-root[data-kb] .w2cmt-cv { opacity: 0; }
        @media (prefers-reduced-motion: reduce) { .w2cmt-cv { display: none; } }
      `}</style>
      <canvas ref={cvRef} className="w2cmt-cv pointer-events-none absolute inset-0 h-full w-full" aria-hidden="true" />
      <div className="relative mx-auto max-w-5xl">
        <p className="text-xs font-semibold uppercase tracking-[0.22em] text-[#f2cf8a]">Kestrel Hill Observatory</p>
        <h2 className="mt-3.5 max-w-[34rem] font-serif text-[clamp(2rem,5vw,3.25rem)] font-normal leading-[1.08]">Public nights on the hill this winter</h2>
        <p className="mt-4 max-w-[30rem] leading-relaxed text-[#c9d3ea]">Bring warm layers. Our volunteers set up the big refractor on the lawn and walk you through whatever the sky gives us.</p>
        <ul className="mt-7 max-w-[26rem] border-t border-[#eef2fb]/20">
          {dates.map((x) => (
            <li key={x.d} className="flex gap-4 border-b border-[#eef2fb]/20 py-3 text-[15px]">
              <span className="w-24 shrink-0 tabular-nums text-[#f2cf8a]">{x.d}</span>{x.what}
            </li>
          ))}
        </ul>
        <a href="#" className="mt-7 inline-flex min-h-[46px] items-center rounded-full bg-[#f2cf8a] px-[22px] text-sm font-bold text-[#0b1630] hover:bg-[#f7dca6] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b1630]">Reserve a telescope slot</a>
      </div>
    </section>
  );
}
```
