List row whose background follows the cursor with a soft radial highlight, written per-element and subtle enough for dense result lists.
npx shadcn@latest add @paragon/spotlight-search-row"use client";
import * as React from "react";
import { useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export interface SpotlightSearchRowProps extends React.ComponentProps<"div"> {
/** Highlight radius in px. */
radius?: number;
/** Highlight intensity, 0–1. */
strength?: number;
/** Disables the spotlight; the row renders unchanged. */
static?: boolean;
}
/**
* A list row whose background softly follows the cursor with a radial
* highlight — subtle enough for data-dense result lists. The cursor position
* is written as custom properties directly on the highlight element (never
* the parent, so there's no style-recalc cascade), and the only transition
* is opacity. Mouse pointers only; reduced motion disables the follow
* entirely and leaves your normal hover styling in charge.
*/
export function SpotlightSearchRow({
radius = 140,
strength = 1,
static: isStatic = false,
className,
children,
onPointerEnter,
onPointerMove,
onPointerLeave,
...props
}: SpotlightSearchRowProps) {
const reducedMotion = useReducedMotion();
const layerRef = React.useRef<HTMLDivElement>(null);
const enabled = !isStatic && !reducedMotion;
const intensity = Math.min(Math.max(strength, 0), 1);
const moveTo = (event: React.PointerEvent<HTMLDivElement>) => {
const layer = layerRef.current;
if (!layer) return;
const rect = event.currentTarget.getBoundingClientRect();
layer.style.setProperty("--spot-x", `${event.clientX - rect.left}px`);
layer.style.setProperty("--spot-y", `${event.clientY - rect.top}px`);
};
return (
<div
data-slot="spotlight-search-row"
className={cn("relative", className)}
onPointerEnter={(event) => {
onPointerEnter?.(event);
if (!enabled || event.pointerType !== "mouse") return;
moveTo(event);
layerRef.current?.setAttribute("data-active", "");
}}
onPointerMove={(event) => {
onPointerMove?.(event);
if (!enabled || event.pointerType !== "mouse") return;
moveTo(event);
}}
onPointerLeave={(event) => {
onPointerLeave?.(event);
layerRef.current?.removeAttribute("data-active");
}}
{...props}
>
<div
ref={layerRef}
aria-hidden
className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 ease-out data-active:opacity-100"
style={{
background: `radial-gradient(${radius}px circle at var(--spot-x, 50%) var(--spot-y, 50%), color-mix(in oklab, var(--color-foreground) ${(6 * intensity).toFixed(1)}%, transparent), transparent 70%)`,
}}
/>
{children}
</div>
);
}