Fazekit

Code

"use client";
import * as React from "react";
import { cn } from "@/lib/utils";

export interface SpotlightGridProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Spotlight colour (any CSS colour). Defaults to the theme primary. */
  color?: string;
  /** Radius of the border glow in px. */
  radius?: number;
  /** Where the light rests before the first hover, as fractions of the grid (null = hidden until hovered). */
  idlePosition?: { x: number; y: number } | null;
  /** Glow strength (0–1) while the pointer is outside the grid. */
  idleOpacity?: number;
}

/**
 * Tracks the pointer once for the whole grid and hands every `SpotlightCard`
 * its own local coordinates — so one light moves across all cards and their borders.
 */
export function SpotlightGrid({
  color = "var(--primary)",
  radius = 360,
  idlePosition = { x: 0.3, y: 0 },
  idleOpacity = 0.55,
  className,
  style,
  children,
  ...props
}: SpotlightGridProps) {
  const ref = React.useRef<HTMLDivElement>(null);
  const frame = React.useRef(0);

  const place = React.useCallback((clientX: number, clientY: number) => {
    const cards = ref.current?.querySelectorAll<HTMLElement>("[data-spotlight-card]") ?? [];
    cards.forEach((card) => {
      const r = card.getBoundingClientRect();
      card.style.setProperty("--spot-x", `${clientX - r.left}px`);
      card.style.setProperty("--spot-y", `${clientY - r.top}px`);
    });
  }, []);

  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    const { clientX, clientY } = e;
    cancelAnimationFrame(frame.current);
    frame.current = requestAnimationFrame(() => place(clientX, clientY));
  };

  // Park the light at its idle position so the grid glows before anyone hovers it.
  const idleX = idlePosition?.x;
  const idleY = idlePosition?.y;
  React.useEffect(() => {
    const el = ref.current;
    if (!el || idleX === undefined || idleY === undefined) return;
    const put = () => {
      const r = el.getBoundingClientRect();
      place(r.left + r.width * idleX, r.top + r.height * idleY);
    };
    put();
    const ro = new ResizeObserver(put);
    ro.observe(el);
    return () => {
      ro.disconnect();
      cancelAnimationFrame(frame.current);
    };
  }, [idleX, idleY, place]);

  return (
    <div
      ref={ref}
      onPointerMove={onPointerMove}
      className={cn("group/spotlight grid gap-4", className)}
      style={
        {
          ...style,
          "--spot-color": color,
          "--spot-radius": `${radius}px`,
          "--spot-idle": idlePosition ? idleOpacity : 0,
        } as React.CSSProperties
      }
      {...props}
    >
      {children}
    </div>
  );
}

export interface SpotlightCardProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Class for the inner surface (padding, layout). */
  contentClassName?: string;
}

export function SpotlightCard({ className, contentClassName, children, ...props }: SpotlightCardProps) {
  return (
    <div
      data-spotlight-card
      className={cn(
        "relative rounded-2xl bg-border p-px",
        "[--spot-x:-999px] [--spot-y:-999px]",
        className,
      )}
      {...props}
    >
      {/* Border glow: shows through the 1px gap around the surface. */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 rounded-[inherit] opacity-[var(--spot-idle,0)] transition-opacity duration-500 group-hover/spotlight:opacity-100"
        style={{
          background:
            "radial-gradient(var(--spot-radius) circle at var(--spot-x) var(--spot-y), var(--spot-color), transparent 50%)",
        }}
      />
      <div className={cn("relative h-full overflow-hidden rounded-[calc(1rem-1px)] bg-card text-card-foreground", contentClassName)}>
        {/* Soft surface light, weaker than the border. */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-0 opacity-[var(--spot-idle,0)] transition-opacity duration-500 group-hover/spotlight:opacity-100"
          style={{
            background:
              "radial-gradient(calc(var(--spot-radius) * 0.9) circle at var(--spot-x) var(--spot-y), color-mix(in oklab, var(--spot-color) 16%, transparent), transparent 60%)",
          }}
        />
        <div className="relative h-full">{children}</div>
      </div>
    </div>
  );
}

More in Cards

View all →