{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pointer-lit-card","type":"registry:component","title":"Pointer Lit Card","description":"A card lit from the pointer: a bright arc of light on the hairline border, a softer wash across the surface, a two-degree tilt toward the pointer and a little counter-parallax on the content.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/pointer-lit-card.tsx","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\n\n/**\n * PointerLitCard — a card lit from the pointer.\n *\n * A bright arc of light on the hairline border and a softer wash across\n * the surface follow the pointer, the card tilts a couple of degrees toward\n * it, and the content drifts a few pixels the other way. Keyboard focus\n * inside lights it from the top. The pointer position is written to CSS\n * variables, so twenty cards cost no re-renders.\n *\n * Part of the Dark Precision kit: OLED black, 1px hairlines, one cool accent\n * used only as light (default #4fd1ff; also mint, amber, white or any\n * #rrggbb). Fonts come from CSS variables with Geist fallbacks. Respects\n * prefers-reduced-motion. No dependencies beyond React.\n */\n\nconst SURFACE = \"#0b0b0d\";\n\nconst LINE_2 = \"rgba(255,255,255,0.14)\";\n\nconst MORPH = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\nconst HEX = /^#[0-9a-fA-F]{6}$/;\n\nexport const DP_ACCENTS = {\n  ice: \"#4fd1ff\",\n  mint: \"#5ef2c1\",\n  amber: \"#ffb45e\",\n  white: \"#f2f4f7\",\n} as const;\n\nfunction own<T extends object>(map: T, key: string): key is Extract<keyof T, string> {\n  return Object.prototype.hasOwnProperty.call(map, key);\n}\n\n/** A named accent or a #rrggbb hex; anything else falls back to ice. */\nfunction accentHex(value: string): string {\n  if (own(DP_ACCENTS, value)) return DP_ACCENTS[value];\n  return HEX.test(value) ? value : DP_ACCENTS.ice;\n}\n\nfunction rgbOf(hex: string): [number, number, number] {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction rgba(hex: string, a: number): string {\n  const [r, g, b] = rgbOf(hex);\n  return `rgba(${r},${g},${b},${a})`;\n}\n\nfunction subscribeMotion(cb: () => void) {\n  const m = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  m.addEventListener(\"change\", cb);\n  return () => m.removeEventListener(\"change\", cb);\n}\n\n/** True when the visitor asked for reduced motion. False on the server. */\nfunction useReducedMotion(): boolean {\n  return useSyncExternalStore(\n    subscribeMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nexport type PointerLitCardProps = {\n  children: ReactNode;\n  accent?: string;\n  tilt?: number;\n  radius?: number;\n  className?: string;\n  style?: CSSProperties;\n};\n\n/**\n * A card lit from the pointer: a bright arc on the hairline border, a softer\n * wash across the surface, a tilt of a couple of degrees and a little\n * parallax on the content. Keyboard focus inside lights it from the top.\n */\nexport function PointerLitCard({\n  children,\n  accent = \"ice\",\n  tilt = 2,\n  radius = 16,\n  className,\n  style,\n}: PointerLitCardProps) {\n  const acc = accentHex(accent);\n  const reduced = useReducedMotion();\n  const set = (el: HTMLElement, v: Record<string, string>) => {\n    for (const k in v) el.style.setProperty(k, v[k]);\n  };\n  const move = (e: ReactPointerEvent<HTMLDivElement>) => {\n    const el = e.currentTarget;\n    const r = el.getBoundingClientRect();\n    const x = Math.min(1, Math.max(0, (e.clientX - r.left) / (r.width || 1)));\n    const y = Math.min(1, Math.max(0, (e.clientY - r.top) / (r.height || 1)));\n    set(el, { \"--mx\": (x * 100).toFixed(2) + \"%\", \"--my\": (y * 100).toFixed(2) + \"%\", \"--lit\": \"1\", \"--td\": \"180ms\" });\n    if (!reduced && tilt > 0) {\n      set(el, {\n        \"--rx\": ((0.5 - y) * tilt * 2).toFixed(3) + \"deg\",\n        \"--ry\": ((x - 0.5) * tilt * 2).toFixed(3) + \"deg\",\n        \"--px\": ((0.5 - x) * 6).toFixed(2) + \"px\",\n        \"--py\": ((0.5 - y) * 6).toFixed(2) + \"px\",\n      });\n    }\n  };\n  const rest = (el: HTMLElement) => set(el, { \"--lit\": \"0\", \"--rx\": \"0deg\", \"--ry\": \"0deg\", \"--px\": \"0px\", \"--py\": \"0px\", \"--td\": \"600ms\" });\n  return (\n    <div\n      onPointerMove={move}\n      onPointerLeave={(e) => rest(e.currentTarget)}\n      onFocus={(e) => set(e.currentTarget, { \"--mx\": \"50%\", \"--my\": \"0%\", \"--lit\": \"1\" })}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) rest(e.currentTarget);\n      }}\n      className={\"relative\" + (className ? \" \" + className : \"\")}\n      style={{\n        borderRadius: radius,\n        padding: 1,\n        background: LINE_2,\n        transform: \"perspective(1000px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg))\",\n        transition: `transform var(--td, 600ms) ${MORPH}`,\n        ...style,\n      }}\n    >\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          borderRadius: radius,\n          background: `radial-gradient(240px circle at var(--mx, 50%) var(--my, 0%), ${rgba(acc, 0.9)}, ${rgba(acc, 0.25)} 40%, transparent 72%)`,\n          opacity: \"var(--lit, 0)\",\n          transition: \"opacity 300ms ease\",\n        }}\n      />\n      <div className=\"relative h-full overflow-hidden\" style={{ borderRadius: radius - 1, background: SURFACE }}>\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0\"\n          style={{\n            background: `radial-gradient(380px circle at var(--mx, 50%) var(--my, 0%), rgba(255,255,255,0.075), transparent 62%), radial-gradient(640px circle at var(--mx, 50%) var(--my, 0%), ${rgba(acc, 0.05)}, transparent 70%)`,\n            opacity: \"var(--lit, 0)\",\n            transition: \"opacity 300ms ease\",\n          }}\n        />\n        <div className=\"relative h-full\" style={{ transform: \"translate3d(var(--px, 0px), var(--py, 0px), 0)\", transition: `transform var(--td, 600ms) ${MORPH}` }}>\n          {children}\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/viberdy/pointer-lit-card.tsx"}],"meta":{"tier":"free","tags":["card","spotlight","border glow","tilt","parallax","pointer","dark","hover"],"href":"https://viberdy.dev/library/pointer-lit-card","aiPrompt":"Build a React + Tailwind card, `PointerLitCard({ children, accent, tilt, radius, className, style })`, lit from the pointer.\n\nStructure: an outer div with 1px padding and a white 14% background (the hairline), a radial-gradient layer on it (240px circle at var(--mx) var(--my): accent 90%, accent 25% at 40%, transparent at 72%) with opacity var(--lit), then an inner surface (#0b0b0d, radius minus 1) holding a wash layer (a 380px white 7.5% radial over a 640px accent 5% radial, same centre, opacity var(--lit)) and the content in a div translated by var(--px) var(--py).\n\nOn pointermove, write --mx and --my as percentages of the card, --lit 1, --rx/--ry (up to `tilt` degrees toward the pointer, via perspective(1000px) rotateX/rotateY on the outer div) and --px/--py (up to 3px the other way); on leave, reset them with a 600ms settle (a --td variable switches the transition from 180ms while moving). Never use React state for the pointer. Focus inside sets the light to the top centre; blur that leaves the card resets it. Reduced motion: light only, no tilt or parallax.\n\nDark Precision grammar: ground #050505, surfaces #0b0b0d and #121215, hairlines white 6% at rest and 14% lit (drawn as inset box-shadows or SVG strokes, never border colours), ink white 92/60/38%, one cool accent used only as light (default #4fd1ff; mint #5ef2c1, amber #ffb45e, white #f2f4f7 or any #rrggbb, validated against /^#[0-9a-fA-F]{6}$/), Geist never heavier than 600 with tight tracking, Geist Mono only for captions and data, fonts through CSS variables with fallbacks (var(--font-sans, \"Geist\", ...)). Morphs use cubic-bezier(0.16, 1, 0.3, 1); ambient loops are 30s or slower. Respect prefers-reduced-motion. No dependencies beyond React.","license":"https://viberdy.dev/license"}}