Holographic Scan Reveal

PreviousNext

component for the Holographic Scan Reveal

Docs
spectrumuicomponent

Preview

Loading preview…
app/registry/animatedtext/components/hologram-text.tsx
"use client"

import type React from "react"
import { motion } from "framer-motion"
import { useMemo } from "react"
import { useTheme } from "next-themes"

type Props = {
  text?: string
  scanDuration?: number
  chromaJitter?: number
}

export function HologramText({
  text = "SPECTRUM UI",
  scanDuration = 1800,
  chromaJitter = 0.8
}: Props) {
  const chars = useMemo(() => text.split(""), [text])
  const { theme } = useTheme()

  // Choose base and glow colors depending on mode
  const baseColor = theme === "dark" ? "#eee" : "#111"
  const glowLight = theme === "dark"
    ? "rgba(255,255,255,0.4)"
    : "rgba(0,0,0,0.25)"
  const glowHeavy = theme === "dark"
    ? "rgba(255,255,255,0.2)"
    : "rgba(0,0,0,0.15)"

  return (
    <motion.div
      className="relative"
      aria-label={text}
      role="img"
      initial={{ opacity: 0, y: 10 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      <div className="flex select-none items-center justify-center gap-[0.04em] font-bold tracking-tight text-[clamp(32px,6vw,72px)]">
        {chars.map((c, i) => (
          <span key={i} className="relative inline-block">
            <span
              className="layer base"
              style={
                {
                  "--i": i,
                  "--scanMs": `${scanDuration}ms`,
                  "--baseColor": baseColor,
                  "--glowLight": glowLight,
                  "--glowHeavy": glowHeavy
                } as React.CSSProperties
              }
            >
              {c === " " ? "\u00A0" : c}
            </span>

            <span
              className="layer red"
              style={
                {
                  "--i": i,
                  "--jit": chromaJitter,
                  "--scanMs": `${scanDuration}ms`,
                } as React.CSSProperties
              }
              aria-hidden="true"
            >
              {c === " " ? "\u00A0" : c}
            </span>
            <span
              className="layer cyan"
              style={
                {
                  "--i": i,
                  "--jit": chromaJitter,
                  "--scanMs": `${scanDuration}ms`,
                } as React.CSSProperties
              }
              aria-hidden="true"
            >
              {c === " " ? "\u00A0" : c}
            </span>

            <span
              className="layer scanline"
              style={
                {
                  "--i": i,
                  "--scanMs": `${scanDuration}ms`,
                } as React.CSSProperties
              }
              aria-hidden="true"
            >
              {c === " " ? "\u00A0" : c}
            </span>
          </span>
        ))}
      </div>

      <style jsx>{`
        .layer {
          position: absolute;
          top: 0;
          left: 0;
          will-change: transform, filter, opacity, clip-path;
        }
        .base {
          position: relative;
          color: var(--baseColor);
          display: inline-block;
          animation: scan var(--scanMs) calc(var(--i) * 45ms) ease-out both,
                     flicker var(--scanMs) calc(var(--i) * 45ms) steps(1) infinite;
          text-shadow: 0 0 8px var(--glowLight), 0 0 16px var(--glowHeavy);
        }

        .red, .cyan {
          display: inline-block;
          color: transparent;
          mix-blend-mode: screen;
          opacity: 0;
          animation:
            chroma var(--scanMs) calc(var(--i) * 45ms) ease-out both,
            jitter calc(var(--scanMs) * 0.8) calc(var(--i) * 45ms) ease-out both;
        }
        .red { text-shadow: 0 0 0 rgba(255, 60, 60, 0.95); }
        .cyan { text-shadow: 0 0 0 rgba(60, 255, 255, 0.95); }

        .scanline {
          display: inline-block;
          color: transparent;
          background: linear-gradient(rgba(255,255,255,0.1) 0%, transparent 50%, rgba(255,255,255,0.1) 100%);
          background-size: 100% 4px;
          animation: scanline-anim var(--scanMs) calc(var(--i) * 45ms) ease-out both;
          mix-blend-mode: overlay;
          opacity: 0;
        }

        @keyframes scan {
          0% {
            color: transparent;
            -webkit-mask-image: linear-gradient(90deg, transparent 40%, #000 50%, transparent 60%);
            mask-image: linear-gradient(90deg, transparent 40%, #000 50%, transparent 60%);
            -webkit-mask-size: 200% 100%;
            mask-size: 200% 100%;
            -webkit-mask-position: -100% 0;
            mask-position: -100% 0;
            letter-spacing: 0.04em;
            filter: contrast(1) brightness(0.5);
            transform: translateY(-4px);
          }
          60% {
            color: var(--baseColor);
            -webkit-mask-position: 100% 0;
            mask-position: 100% 0;
            letter-spacing: 0.005em;
            filter: contrast(1.2) brightness(1.1);
            transform: translateY(0);
          }
          100% {
            color: var(--baseColor);
            -webkit-mask-image: none;
            mask-image: none;
            letter-spacing: 0;
            filter: none;
            transform: translateY(0);
          }
        }
        @keyframes chroma {
          0% { opacity: 0; }
          30% { opacity: 0.8; }
          60% { opacity: 0.45; }
          100% { opacity: 0; }
        }
        @keyframes jitter {
          0% {
            transform: translate(
              calc((var(--jit) * 1px) * (cos(var(--i)))),
              calc((var(--jit) * 1px) * (sin(var(--i))))
            );
            filter: blur(0.5px);
          }
          50% {
            transform: translate(
              calc((var(--jit) * 4px) * (cos(var(--i) * 3.1))),
              calc((var(--jit) * 3px) * (sin(var(--i) * 2.6)))
            );
            filter: blur(1.2px);
          }
          100% {
            transform: translate(0,0);
            filter: blur(0);
          }
        }
        @keyframes flicker {
          0%, 100% { opacity: 1; }
          2% { opacity: 0.9; }
          4% { opacity: 1; }
          6% { opacity: 0.95; }
          8% { opacity: 1; }
          10% { opacity: 0.9; }
          12% { opacity: 1; }
          14% { opacity: 0.92; }
          16% { opacity: 1; }
          18% { opacity: 0.98; }
          20% { opacity: 1; }
          22% { opacity: 0.9; }
          24% { opacity: 1; }
          26% { opacity: 0.95; }
          28% { opacity: 1; }
          30% { opacity: 0.9; }
          32% { opacity: 1; }
          34% { opacity: 0.92; }
          36% { opacity: 1; }
          38% { opacity: 0.98; }
          40% { opacity: 1; }
          42% { opacity: 0.9; }
          44% { opacity: 1; }
          46% { opacity: 0.95; }
          48% { opacity: 1; }
          50% { opacity: 0.9; }
          52% { opacity: 1; }
          54% { opacity: 0.92; }
          56% { opacity: 1; }
          58% { opacity: 0.98; }
          60% { opacity: 1; }
          62% { opacity: 0.9; }
          64% { opacity: 1; }
          66% { opacity: 0.95; }
          68% { opacity: 1; }
          70% { opacity: 0.9; }
          72% { opacity: 1; }
          74% { opacity: 0.92; }
          76% { opacity: 1; }
          78% { opacity: 0.98; }
          80% { opacity: 1; }
          82% { opacity: 0.9; }
          84% { opacity: 1; }
          86% { opacity: 0.95; }
          88% { opacity: 1; }
          90% { opacity: 0.9; }
          92% { opacity: 1; }
          94% { opacity: 0.92; }
          96% { opacity: 1; }
          98% { opacity: 0.98; }
        }
        @keyframes scanline-anim {
          0% {
            transform: translateY(-100%);
            opacity: 0;
          }
          30% {
            opacity: 0.6;
          }
          60% {
            transform: translateY(100%);
            opacity: 0.3;
          }
          100% {
            opacity: 0;
          }
        }
      `}</style>
    </motion.div>
  )
}

Installation

npx shadcn@latest add @spectrumui/holographic

Usage

import { Holographic } from "@/components/holographic"
<Holographic />