Luminous Shelf Card

Luminous Shelf Card

Hover Lighting

Dark product card with an under-shelf light that strikes on hover — a volumetric cone washes up the wall, the shelf edge becomes a hot filament, and the feather pin is lit from below while casting a soft shadow upward.

Interactive Playground & Studio

Live Controls
luminous-shelf-card.tsx
Light Work
Life life on easy mode by UI Pirate
Hover or click to interact
Customizer
PRESETS & VARIATIONS

All Variants & Themes

Click to test interactive spring physics on every theme

Studio Whitetheme="white"
Light Work
Life life on easy mode.

Neutral broadcast key-light — the reference look.

Incandescenttheme="warm"
Light Work
Warm tungsten glow.

Tungsten bulb warmth pooling up the wall.

Arctic Cyantheme="arctic"
Light Work
Cold clean-room light.

Cold clean-room strip lighting.

Amber Sodiumtheme="amber"
Light Work
Sodium street glow.

Street-lamp sodium orange bloom.

Emerald Labtheme="emerald"
Light Work
Phosphor reactor wash.

Phosphor reactor green wash.

Neon Magentatheme="magenta"
Light Work
Nightclub tube strike.

Nightclub magenta tube strike.

Features & Physics

Volumetric upward light cone (screen-blended layers)
Shelf edge strikes into a hot filament line
Feather pin lit from below, casts shadow upward
Fluorescent-tube flicker on turn-on

Code & Integration

Customized live with your active playground prop settings:

"use client";

import React, { useState } from "react";
import { motion } from "framer-motion";

export type LuminousShelfCardStateMode = "interactive" | "standerd" | "hover";
export type LuminousShelfCardSize = "sm" | "md" | "lg";
export type LuminousShelfCardTheme =
  | "white" | "warm" | "arctic" | "amber" | "emerald" | "magenta";

const THEMES = {
  white:   { wash: "236, 238, 245", core: "#ffffff", litTint: "244, 247, 255" },
  warm:    { wash: "255, 216, 168", core: "#fff3de", litTint: "255, 236, 208" },
  arctic:  { wash: "178, 228, 255", core: "#eefbff", litTint: "226, 245, 255" },
  amber:   { wash: "255, 196, 122", core: "#ffe9cc", litTint: "255, 228, 196" },
  emerald: { wash: "170, 240, 208", core: "#ecfff6", litTint: "228, 252, 240" },
  magenta: { wash: "255, 184, 230", core: "#ffecf9", litTint: "255, 230, 248" },
} as const;

const SCALE = { sm: 0.78, md: 1, lg: 1.16 } as const;
const CARD_W = 340, CARD_H = 460, R = 28;
const PIN = 104, PIN_TOP = 96;
const SHELF_W = 250, SHELF_H = 14, SHELF_TOP = 288;
const SFB = CARD_H - SHELF_TOP; // shelf distance from bottom

export interface LuminousShelfCardProps {
  title?: string;
  subtitle?: string;
  stateMode?: LuminousShelfCardStateMode;
  size?: LuminousShelfCardSize;
  theme?: LuminousShelfCardTheme;
  icon?: React.ReactNode;
  showDeviceFrame?: boolean;
  showPeekPill?: boolean;
  flicker?: boolean;
  onClick?: () => void;
  className?: string;
}

// Default glyph = the UI Pirate brand mark (same asset the site navbar uses).
const MARK_SRC = "https://res.cloudinary.com/dvk9ttiym/image/upload/v1766234689/logo_lcn2cq.png";
const Mark = ({ s }: { s: number }) => (
  <img src={MARK_SRC} alt="" width={s} height={s}
    style={{ width: s, height: s, objectFit: "contain", display: "block" }} />
);

export function LuminousShelfCard({
  title = "Light Work",
  subtitle = "Life life on easy mode by UI Pirate",
  stateMode = "interactive",
  size = "md",
  theme = "white",
  icon,
  showDeviceFrame = true,
  showPeekPill = true,
  flicker = true,
  onClick,
  className = "",
}: LuminousShelfCardProps) {
  const [hovered, setHovered] = useState(false);
  const [focused, setFocused] = useState(false);
  const t = THEMES[theme] ?? THEMES.white;
  const scale = SCALE[size] ?? 1;
  const clickable = typeof onClick === "function";
  const on = stateMode === "hover" || (stateMode === "interactive" && (hovered || focused));

  const litTransition = on && flicker
    ? { duration: 0.55, times: [0, 0.14, 0.24, 1], ease: "easeOut" as const }
    : { duration: on ? 0.45 : 0.32, ease: "easeOut" as const };
  const lit = (peak: number): number | number[] =>
    on ? (flicker ? [0, peak, peak * 0.5, peak] : peak) : 0;

  return (
    <div
      className={`relative flex items-center justify-center select-none ${clickable ? "cursor-pointer" : ""} ${className}`}
      style={{ width: CARD_W * scale, height: CARD_H * scale }}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onFocus={() => setFocused(true)}
      onBlur={() => setFocused(false)}
      onClick={onClick}
      role={clickable ? "button" : "group"}
      aria-label={title}
      tabIndex={clickable ? 0 : undefined}
    >
      <div className="relative flex-none" style={{ width: CARD_W, height: CARD_H, transform: `scale(${scale})` }}>
        {/* 1 · blueprint frame */}
        {showDeviceFrame && (
          <motion.div className="absolute inset-0 pointer-events-none"
            animate={{ opacity: on ? 0.6 : 0.32 }} transition={{ duration: 0.5 }}>
            <div className="absolute" style={{ inset: -13, borderRadius: R + 13, border: "1px solid rgba(255,255,255,0.05)" }} />
            <div className="absolute" style={{ top: -21, left: 32, width: 60, height: 32, borderRadius: "16px 16px 16px 4px", border: "1px solid rgba(255,255,255,0.055)", borderBottomColor: "transparent" }} />
            <div className="absolute" style={{ top: -21, right: 28, width: 42, height: 42, borderRadius: 12, border: "1px solid rgba(255,255,255,0.055)" }} />
          </motion.div>
        )}

        {/* 2 · card body */}
        <div className="absolute inset-0 overflow-hidden" style={{
          borderRadius: R,
          background: "linear-gradient(180deg, #202024 0%, #171719 50%, #0c0c0e 100%)",
          border: "1px solid rgba(255,255,255,0.07)",
          boxShadow: "0 40px 80px -24px rgba(0,0,0,0.72), inset 0 1px 0 rgba(255,255,255,0.05)",
        }}>
          <div className="absolute inset-0 pointer-events-none" style={{ background: "radial-gradient(58% 40% at 50% 26%, rgba(255,255,255,0.03), transparent 72%)" }} />

          {/* 3 · diffuse wall glow — soft dome + band, NO hard cone */}
          <motion.div className="absolute pointer-events-none left-1/2 -translate-x-1/2" style={{
            bottom: SFB - 34, width: 410, height: 400, mixBlendMode: "screen", filter: "blur(26px)",
            background: `radial-gradient(ellipse 60% 80% at 50% 100%, rgba(${t.wash},0.27) 0%, rgba(${t.wash},0.14) 36%, rgba(${t.wash},0.045) 64%, rgba(${t.wash},0) 84%)`,
          }} animate={{ opacity: lit(1) }} transition={litTransition} />
          <motion.div className="absolute pointer-events-none left-1/2 -translate-x-1/2" style={{
            bottom: SFB - 16, width: 300, height: 150, mixBlendMode: "screen", filter: "blur(18px)",
            background: `radial-gradient(ellipse 62% 72% at 50% 100%, rgba(${t.wash},0.36) 0%, rgba(${t.wash},0.11) 48%, rgba(${t.wash},0) 78%)`,
          }} animate={{ opacity: lit(1) }} transition={litTransition} />
          <motion.div className="absolute pointer-events-none left-1/2 -translate-x-1/2" style={{
            top: SHELF_TOP + SHELF_H, width: 230, height: 60, mixBlendMode: "screen", filter: "blur(10px)",
            background: `radial-gradient(ellipse 60% 100% at 50% 0%, rgba(${t.wash},0.12) 0%, rgba(${t.wash},0) 74%)`,
          }} animate={{ opacity: lit(1) }} transition={litTransition} />

          {/* 4 · pin cast shadow — a real silhouette of the pin projected up
                 the wall, with the feather punched back out (screen) so the
                 light through its gaps splits the shadow into two humps */}
          <motion.div className="absolute pointer-events-none left-1/2" style={{
            top: PIN_TOP - 30, width: PIN, height: PIN,
            transformOrigin: "50% 100%", transform: "translateX(-50%) scale(1.12, 1.4)", filter: "blur(12px)",
          }} animate={{ opacity: on ? 1 : 0 }} transition={{ duration: on ? 0.5 : 0.3 }}>
            <div className="absolute inset-0" style={{ borderRadius: "50% 50% 50% 13px", background: "rgba(0,0,0,0.6)" }} />
            <div className="absolute flex items-center justify-center" style={{
              left: "50%", top: "48%", width: PIN * 0.56, height: PIN * 0.56,
              transform: "translate(-50%, -50%)", mixBlendMode: "screen", filter: "brightness(0) invert(1)",
            }}>
              {icon ?? <Mark s={PIN * 0.56} />}
            </div>
          </motion.div>

          {/* 2b · edge vignette (always) */}
          <div className="absolute inset-0 pointer-events-none" style={{ background: "radial-gradient(115% 75% at 50% 46%, transparent 52%, rgba(0,0,0,0.44) 100%)" }} />

          {/* 5 · mark pin — pops forward on hover */}
          <motion.div className="absolute left-1/2 flex items-center justify-center" style={{
            top: PIN_TOP, width: PIN, height: PIN, borderRadius: "50% 50% 50% 13px",
            background: "linear-gradient(150deg, #d9d9d9 0%, #b2b2b2 52%, #9a9a9a 100%)", transformOrigin: "50% 60%",
          }} animate={{
            x: "-50%", y: on ? -5 : 0, scale: on ? 1.035 : 1,
            boxShadow: on
              ? "0 -3px 22px -6px rgba(255,255,255,0.12), inset 0 -8px 15px rgba(255,255,255,0.28), inset 0 8px 14px rgba(0,0,0,0.24)"
              : "0 20px 36px -14px rgba(0,0,0,0.55), inset 0 1px 1px rgba(255,255,255,0.5), inset 0 -6px 12px rgba(0,0,0,0.16)",
          }} transition={{ duration: 0.45, ease: [0.2, 0.8, 0.2, 1] }}>
            <motion.div className="absolute inset-0 pointer-events-none" style={{ borderRadius: "inherit",
              background: `radial-gradient(ellipse 90% 66% at 50% 118%, rgba(${t.litTint},0.85) 0%, rgba(${t.litTint},0) 62%)` }}
              animate={{ opacity: on ? 1 : 0 }} transition={{ duration: 0.45 }} />
            <motion.div className="absolute inset-0 pointer-events-none" style={{ borderRadius: "inherit",
              background: "linear-gradient(180deg, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0) 48%)" }}
              animate={{ opacity: on ? 0.4 : 1 }} transition={{ duration: 0.45 }} />
            <div className="relative flex items-center justify-center" style={{ width: PIN * 0.6, height: PIN * 0.6 }}>{icon ?? <Mark s={PIN * 0.6} />}</div>
          </motion.div>

          {/* 6 · shelf / light bar */}
          <div className="absolute left-1/2 -translate-x-1/2" style={{ top: SHELF_TOP, width: SHELF_W, height: SHELF_H }}>
            <motion.div className="absolute inset-0" style={{
              clipPath: "polygon(0 0, 100% 0, calc(100% - 11px) 100%, 11px 100%)",
              boxShadow: "0 12px 22px -6px rgba(0,0,0,0.55)",
            }} animate={{
              background: on
                ? "linear-gradient(180deg, #2b2b30 0%, #191a1c 46%, #0e0e10 100%)"
                : "linear-gradient(180deg, #202024 0%, #16161a 44%, #0b0b0d 100%)",
            }} transition={{ duration: 0.4 }} />
            <div className="absolute left-0 right-0 top-0" style={{
              height: 2,
              background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.2) 18%, rgba(255,255,255,0.28) 50%, rgba(255,255,255,0.2) 82%, transparent)",
              opacity: on ? 0 : 1, transition: "opacity 0.35s ease",
            }} />
            <motion.div className="absolute left-0 right-0" style={{
              top: -0.5, height: 2, borderRadius: 2, background: t.core, boxShadow: `0 0 5px 0 rgba(${t.wash},0.7)`,
            }} animate={{ opacity: lit(1) }} transition={litTransition} />
            <motion.div className="absolute" style={{
              left: -12, right: -12, top: -12, height: 26, filter: "blur(12px)", mixBlendMode: "screen",
              background: `radial-gradient(ellipse 72% 100% at 50% 50%, rgba(${t.wash},0.8) 0%, rgba(${t.wash},0) 74%)`,
            }} animate={{ opacity: lit(1) }} transition={litTransition} />
            <motion.div className="absolute" style={{
              left: "6%", right: "6%", top: SHELF_H + 2, height: 2, borderRadius: 2, filter: "blur(1.5px)", mixBlendMode: "screen",
              background: `linear-gradient(90deg, transparent, rgba(${t.wash},0.7) 28%, rgba(${t.wash},0.7) 72%, transparent)`,
            }} animate={{ opacity: lit(0.9) }} transition={litTransition} />
          </div>

          {/* 7 · text */}
          <div className="absolute" style={{ left: 26, bottom: 24, right: 0 }}>
            <motion.div style={{ fontSize: 28, lineHeight: 1.05, fontWeight: 600, letterSpacing: "-0.015em" }}
              animate={{ color: on ? "#f4f4f4" : "#e3e3e3", textShadow: on ? `0 0 16px rgba(${t.wash},0.3)` : "0 0 0 rgba(0,0,0,0)" }}
              transition={{ duration: 0.4 }}>{title}</motion.div>
            <motion.div className="mt-2 whitespace-nowrap" style={{ fontSize: 15 }}
              animate={{ color: on ? "#8c8c8c" : "#555555" }} transition={{ duration: 0.4 }}>{subtitle}</motion.div>
          </div>

          {/* 8 · peek pill */}
          {showPeekPill && (
            <motion.div className="absolute" style={{
              right: 24, bottom: -13, width: 88, height: 32, borderRadius: 999,
              background: "linear-gradient(180deg, #fbfbfb 0%, #e3e3e3 100%)",
              boxShadow: "0 -6px 15px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.9)",
            }} animate={{ y: on ? -3 : 0, filter: on ? "brightness(1.06)" : "brightness(1)" }}
              transition={{ type: "spring", stiffness: 320, damping: 26 }} />
          )}
        </div>
      </div>
    </div>
  );
}

export default LuminousShelfCard;

Peer Dependencies

Requires Framer Motion and Tailwind CSS for spring physics and layout tokens:

npm install framer-motion clsx lucide-react tailwind-merge

API & Props Reference

PropTypeDefaultDescription
titlestring"Light Work"Large heading, bottom-left.
subtitlestring"Life life on easy mode by UI Pirate"One-line sub-text; clips at the card edge like the reference.
stateMode"interactive" | "standerd" | "hover""interactive""standerd" = light off · "hover" = light on · "interactive" = follows pointer / focus.
size"sm" | "md" | "lg""md"Uniform scale of the whole card.
theme"white" | "warm" | "arctic" | "amber" | "emerald" | "magenta""white"Colour of the light that strikes on hover.
iconReact.ReactNode<Feather />Glyph inside the pin.
showDeviceFramebooleantrueFaint blueprint frame + corner brackets behind the card.
showPeekPillbooleantrueWhite pill peeking up from the bottom-right edge.
flickerbooleantrueFluorescent-tube strike flicker when the light turns on.
onClick() => voidundefinedFired on click / Enter / Space when set.