Spotlight Content

PreviousNext

Spotlight content effect applied to a container.

Docs
phucbmcomponent

Preview

Loading preview…
registry/phucbm/blocks/spotlight-content/spotlight-content.tsx
"use client";

import gsap from "gsap";
import { Observer } from "gsap/Observer";
import { useRef } from "react";
import { useGSAP } from "@gsap/react";
import { getNormalizedMousePosition } from "normalized-mouse-position";
import clsx from "clsx";

gsap.registerPlugin(Observer);

export type SpotlightContentProps = {
  /** Background color or gradient for the spotlight area. @default "#fbbf24" */
  color?: string;
  /** Duration of the smooth motion in seconds. @default 0.4 */
  duration?: number;
  /** Easing function used for the tween animation. @default "power2.out" */
  ease?: string;
  /** Vertical clip range in percent [min, max]. @default [20, 70] */
  rangeY?: [number, number];
  /** Horizontal clip range in percent [min, max]. @default [20, 70] */
  rangeX?: [number, number];
  /** Reverse the X-axis direction when moving the mouse. @default true */
  reverseX?: boolean;
  /** Initial clip position before any mouse movement. @default {x:45, y:45} */
  initial?: { x: number; y: number };
  /** Additional class names applied to the root section. */
  className?: string;
};

export function SpotlightContent({
  color = "#fbbf24",
  duration = 0.4,
  ease = "power2.out",
  rangeY = [20, 70],
  rangeX = [20, 70],
  reverseX = true,
  initial = { x: 45, y: 45 },
  className,
}: SpotlightContentProps) {
  const scope = useRef<HTMLElement | null>(null);
  const clipDiv = useRef<HTMLDivElement | null>(null);

  useGSAP(() => {
    const el = clipDiv.current;
    if (!el) return;

    const mapRange = (
      v: number,
      inMin: number,
      inMax: number,
      outMin: number,
      outMax: number
    ) => ((v - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;

    const clipState = { x: initial.x, y: initial.y };
    const updateClipPath = () => {
      gsap.set(el, {
        clipPath: `polygon(74% 0, 100% 0, 100% 23%, ${clipState.x}% 100%, 0 100%, 0 ${clipState.y}%)`,
      });
    };

    const tweenX = gsap.quickTo(clipState, "x", {
      duration,
      ease,
      onUpdate: updateClipPath,
    });
    const tweenY = gsap.quickTo(clipState, "y", {
      duration,
      ease,
      onUpdate: updateClipPath,
    });

    updateClipPath();

    const observer = Observer.create({
      target: window,
      type: "pointer",
      onMove: ({ x = 0, y = 0 }) => {
        const pos = getNormalizedMousePosition({
          x,
          y,
          origin: "100% 0%",
          clamp: true,
        });

        const mappedX = reverseX ? 1 - Math.abs(pos.x) : Math.abs(pos.x);
        const clipX = mapRange(mappedX, 0, 1, rangeX[0], rangeX[1]);
        const clipY = mapRange(pos.y, 0, 1, rangeY[0], rangeY[1]);

        tweenX(clipX);
        tweenY(clipY);
      },
    });

    return () => observer.kill();
  }, [duration, ease, rangeX, rangeY, reverseX]);

  return (
    <section
      ref={scope}
      className={clsx("h-screen overflow-hidden", className)}
    >
      <div
        ref={clipDiv}
        className="h-full"
        style={{
          background: color,
          clipPath: `polygon(74% 0, 100% 0, 100% 23%, ${initial.x}% 100%, 0 100%, 0 ${initial.y}%)`,
        }}
      />
    </section>
  );
}

Installation

npx shadcn@latest add @phucbm/spotlight-content

Usage

import { SpotlightContent } from "@/components/spotlight-content"
<SpotlightContent />