# CTA Dark Spotlight — Tailwind CTA Section

> An always-dark closing section lit by a cursor torch: the headline ink and a dot grid brighten where the light falls, and it drifts on its own when no pointer is around.

- Category: [Calls to Action](https://pixelpapa.com/components/cta.md)
- Tags: cta, dark, spotlight, cursor, pointer
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/cta/cta-dark-spotlight

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/cta-dark-spotlight.json"
```

The file is written to `components/pixelpapa/cta-dark-spotlight.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ArrowRight } from "lucide-react";
import {
  motion,
  useAnimationFrame,
  useInView,
  useMotionTemplate,
  useMotionValue,
  useReducedMotion,
  useSpring,
  useTransform,
} from "motion/react";
import { useRef, type PointerEvent } from "react";

type Stat = { value: string; label: string };

type CtaDarkSpotlightProps = {
  eyebrow?: string;
  title?: string;
  body?: string;
  primaryLabel?: string;
  secondaryLabel?: string;
  stats?: Stat[];
};

const defaultStats: Stat[] = [
  { value: "38 ms", label: "median trace ingest" },
  { value: "1.2 B", label: "spans indexed daily" },
  { value: "52 min", label: "to a first fixed regression" },
];

const ease = [0.22, 1, 0.36, 1] as const;
const dots = (alpha: number) => `radial-gradient(oklch(1 0 0 / ${alpha}) 1px, transparent 1.5px)`;

export default function CtaDarkSpotlight({
  eyebrow = "Meridian · tracing for busy backends",
  title = "Find the slow line before your customers find it for you",
  body = "Meridian follows every request across services and points at the query, the retry or the cold start that cost you the second.",
  primaryLabel = "Start tracing free",
  secondaryLabel = "Read the docs",
  stats = defaultStats,
}: CtaDarkSpotlightProps) {
  const reduce = useReducedMotion();
  const sectionRef = useRef<HTMLElement>(null);
  const headingRef = useRef<HTMLHeadingElement>(null);
  const inView = useInView(sectionRef);
  const hovering = useRef(false);
  // Where the heading sits inside the section, so its ink gradient shares the torch's coordinates.
  const origin = useRef({ x: 0, y: 0 });

  const x = useMotionValue(680);
  const y = useMotionValue(280);
  const sx = useSpring(x, { stiffness: 110, damping: 22, mass: 0.7 });
  const sy = useSpring(y, { stiffness: 110, damping: 22, mass: 0.7 });
  const hx = useTransform(sx, (v) => v - origin.current.x);
  const hy = useTransform(sy, (v) => v - origin.current.y);

  const glow = useMotionTemplate`radial-gradient(620px circle at ${sx}px ${sy}px, color-mix(in oklch, var(--primary) 42%, transparent), transparent 70%)`;
  const dotMask = useMotionTemplate`radial-gradient(380px circle at ${sx}px ${sy}px, black 10%, transparent 100%)`;
  const ink = useMotionTemplate`radial-gradient(440px circle at ${hx}px ${hy}px, oklch(1 0 0) 0%, oklch(1 0 0 / 0.3) 100%)`;

  function measure() {
    const s = sectionRef.current?.getBoundingClientRect();
    const h = headingRef.current?.getBoundingClientRect();
    if (s && h) origin.current = { x: h.left - s.left, y: h.top - s.top };
    return s;
  }

  // With no pointer the light wanders in a slow figure-eight over the headline.
  useAnimationFrame((time) => {
    if (hovering.current || !inView) return;
    const s = measure();
    if (!s) return;
    const t = reduce ? 0 : time;
    x.set(s.width / 2 + Math.sin(t / 2600) * Math.min(s.width * 0.24, 320));
    y.set(s.height * 0.4 + Math.sin(t / 1300) * s.height * 0.07);
  });

  function onPointerMove(e: PointerEvent<HTMLElement>) {
    const s = measure();
    if (!s) return;
    hovering.current = true;
    x.set(e.clientX - s.left);
    y.set(e.clientY - s.top);
  }

  return (
    <section
      ref={sectionRef}
      onPointerMove={onPointerMove}
      onPointerLeave={() => {
        hovering.current = false;
      }}
      className="relative isolate overflow-hidden bg-[oklch(0.13_0.02_265)] px-6 py-28 text-white sm:py-36"
    >
      <div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
        <div className="absolute inset-0 [background-size:26px_26px]" style={{ backgroundImage: dots(0.08) }} />
        <motion.div
          className="absolute inset-0 [background-size:26px_26px]"
          style={{ backgroundImage: dots(0.7), maskImage: dotMask, WebkitMaskImage: dotMask }}
        />
        <motion.div className="absolute inset-0" style={{ backgroundImage: glow }} />
        <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/25 to-transparent" />
        <div className="absolute inset-0 [background:radial-gradient(120%_90%_at_50%_40%,transparent_55%,oklch(0.09_0.02_265))]" />
      </div>

      <div className="mx-auto flex max-w-5xl flex-col items-center text-center">
        <motion.p
          initial={{ opacity: 0, y: 10 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease }}
          className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs font-medium text-white/65 backdrop-blur"
        >
          <span className="size-1.5 rounded-full bg-emerald-400 shadow-[0_0_10px_2px] shadow-emerald-400/60" />
          {eyebrow}
        </motion.p>

        <motion.h2
          ref={headingRef}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 0.9, delay: 0.05 }}
          style={{ backgroundImage: ink }}
          className="mt-7 max-w-4xl bg-clip-text pb-2 text-5xl font-semibold tracking-tight text-balance text-transparent sm:text-7xl"
        >
          {title}
        </motion.h2>

        <motion.p
          initial={{ opacity: 0, y: 14 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, delay: 0.2, ease }}
          className="mt-6 max-w-xl text-lg text-pretty text-white/60"
        >
          {body}
        </motion.p>

        <motion.div
          initial={{ opacity: 0, y: 14 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, delay: 0.3, ease }}
          className="mt-10 flex flex-wrap items-center justify-center gap-3"
        >
          <a
            href="#"
            className="group inline-flex h-12 items-center gap-2 rounded-full bg-white px-6 text-sm font-semibold text-[oklch(0.16_0.02_265)] shadow-[0_0_40px_-6px] shadow-white/50 outline-offset-4 outline-white transition-transform hover:-translate-y-0.5"
          >
            {primaryLabel}
            <ArrowRight className="size-4 transition-transform duration-300 group-hover:translate-x-1" />
          </a>
          <a
            href="#"
            className="inline-flex h-12 items-center rounded-full border border-white/15 bg-white/5 px-6 text-sm font-medium text-white outline-offset-4 outline-white backdrop-blur transition-colors hover:bg-white/10"
          >
            {secondaryLabel}
          </a>
        </motion.div>

        <motion.dl
          initial="hidden"
          animate="shown"
          transition={{ staggerChildren: 0.1, delayChildren: 0.45 }}
          className="mt-16 grid w-full max-w-3xl grid-cols-1 gap-y-6 border-t border-white/10 pt-8 sm:grid-cols-3 sm:divide-x sm:divide-white/10"
        >
          {stats.map((stat) => (
            <motion.div
              key={stat.label}
              variants={{ hidden: { opacity: 0, y: 10 }, shown: { opacity: 1, y: 0 } }}
              className="flex flex-col-reverse gap-1 px-4"
            >
              <dt className="text-sm text-white/45">{stat.label}</dt>
              <dd className="text-2xl font-semibold tracking-tight text-white tabular-nums">{stat.value}</dd>
            </motion.div>
          ))}
        </motion.dl>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "CTA Dark Spotlight" component from PixelPapa UI to this project.

What it is: An always-dark closing section lit by a cursor torch: the headline ink and a dot grid brighten where the light falls, and it drifts on its own when no pointer is around.
Category: Calls to Action. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/cta-dark-spotlight.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/cta-dark-spotlight.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/cta-dark-spotlight" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More calls to action

- [CTA App Download](https://pixelpapa.com/components/cta/cta-app-download.md) (Pro): App download panel with generic store buttons, a scannable-looking code, and a phone that tilts toward the cursor while its route draws itself and a map saves offline.
- [CTA Countdown Launch](https://pixelpapa.com/components/cta/cta-countdown-launch.md) (Pro): A launch countdown on split-flap tiles whose digits roll over each second, with orbit rings turning behind it and a waitlist field that confirms your place inline.
- [CTA Gradient Glow Card](https://pixelpapa.com/components/cta/cta-gradient-glow-card.md) (Pro): A closing card ringed by a slowly rotating gradient border, with a soft light that trails the cursor and a copyable install command.
- [CTA Marquee Text Band](https://pixelpapa.com/components/cta/cta-marquee-text-band.md): Three rows of giant type drift in opposite directions behind one button; they speed up and lean with page scroll and slow to a crawl while you hover the button.
- [CTA Minimal Serif](https://pixelpapa.com/components/cta/cta-minimal-serif.md): One large serif sentence that rises word by word, closed by a magnetic link: a ring of turning type leans toward the cursor while the label's underline draws in.
- [CTA Split With Form](https://pixelpapa.com/components/cta/cta-split-with-form.md) (Pro): Pitch and a what-happens-next timeline on the left, a demo request form on the right that validates, sends and morphs into a drawn-check confirmation.
