{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "holographic-card",
  "title": "Holographic Card",
  "description": "A trading-card holo effect — iridescent foil, specular glare, and glitter that tilt and shift with the pointer or device gyroscope.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/holographic-card/holographic-card.tsx",
      "content": "\"use client\";\n\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type HolographicVariant = \"rainbow\" | \"aurora\" | \"galaxy\" | \"gold\";\n\nexport type HolographicCardProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Foil colorway. */\n  variant?: HolographicVariant;\n  /** Maximum tilt in degrees toward the pointer. */\n  maxTilt?: number;\n  /** Render a specular glare that tracks the pointer. */\n  glare?: boolean;\n  /** Overlay a fine glitter mask for a holo-flake finish. */\n  sparkle?: boolean;\n  /** Drive the tilt from the device gyroscope on touch devices. */\n  gyroscope?: boolean;\n};\n\n// SPRING.bouncy — lively follow-through for pointer-tracked motion.\nconst SPRING = { stiffness: 170, damping: 12, mass: 0.1 } as const;\n\nconst ROOT_BASE =\n  \"relative isolate overflow-hidden rounded-2xl border border-white/10 text-white shadow-2xl [will-change:transform]\";\n\n// A dark, saturated base so the color-dodge foil reads as vivid iridescence\n// rather than blowing out to white over a light surface.\nconst BASE: Record<HolographicVariant, string> = {\n  rainbow:\n    \"[background:radial-gradient(120%_120%_at_30%_15%,#312e81_0%,#0b1020_55%,#020617_100%)]\",\n  aurora:\n    \"[background:radial-gradient(120%_120%_at_30%_15%,#0e7490_0%,#052e2b_55%,#020617_100%)]\",\n  galaxy:\n    \"[background:radial-gradient(120%_120%_at_30%_15%,#4c1d95_0%,#0a0a14_55%,#020617_100%)]\",\n  gold: \"[background:radial-gradient(120%_120%_at_30%_15%,#78350f_0%,#1c1206_55%,#020617_100%)]\",\n};\n\n// The iridescent foil per colorway. Hardcoded hues are intrinsic to the effect\n// (like aurora-text / light-rays), not theme surfaces — the blend mode fuses\n// them with the dark base.\nconst FOIL: Record<HolographicVariant, string> = {\n  rainbow:\n    \"[background-image:linear-gradient(115deg,#ff2d95,#ffd84d,#4dff9e,#4dd2ff,#a24dff,#ff2d95)]\",\n  aurora:\n    \"[background-image:linear-gradient(115deg,#22d3ee,#34d399,#a3e635,#38bdf8,#22d3ee)]\",\n  galaxy:\n    \"[background-image:linear-gradient(115deg,#a855f7,#ec4899,#3b82f6,#c026d3,#a855f7)]\",\n  gold: \"[background-image:linear-gradient(115deg,#b45309,#fbbf24,#fffbeb,#fbbf24,#b45309)]\",\n};\n\n// Foil concentrates where the \"light\" (pointer) hits, so it catches highlights\n// like real holo film instead of flooding the whole card.\nconst FOIL_LAYER =\n  \"pointer-events-none absolute inset-0 mix-blend-color-dodge [background-size:200%_200%] [background-position:var(--holo-x)_var(--holo-y)] [mask-image:radial-gradient(75%_75%_at_var(--holo-x)_var(--holo-y),#000_0%,rgba(0,0,0,0.4)_55%,transparent_100%)]\";\n\nconst SPARKLE_LAYER =\n  \"pointer-events-none absolute inset-0 opacity-40 mix-blend-color-dodge [background-image:radial-gradient(rgba(255,255,255,0.9)_0.5px,transparent_1.6px)] [background-size:6px_6px] [mask-image:radial-gradient(45%_45%_at_var(--holo-x)_var(--holo-y),#000,transparent_75%)]\";\n\nconst GLARE_LAYER =\n  \"pointer-events-none absolute inset-0 z-overlay mix-blend-soft-light [background:radial-gradient(40%_40%_at_var(--holo-x)_var(--holo-y),rgba(255,255,255,0.55),transparent_72%)]\";\n\n// Premium edge: a crisp top light-line plus an inner vignette to seat the card.\nconst EDGE_LAYER =\n  \"pointer-events-none absolute inset-0 z-overlay rounded-[inherit] [box-shadow:inset_0_1px_0_0_rgba(255,255,255,0.18),inset_0_0_36px_0_rgba(0,0,0,0.45)]\";\n\nconst HolographicCard = React.forwardRef<HTMLDivElement, HolographicCardProps>(\n  (\n    {\n      variant = \"rainbow\",\n      maxTilt = 14,\n      glare = true,\n      sparkle = true,\n      gyroscope = false,\n      className,\n      style,\n      children,\n      onPointerMove,\n      onPointerLeave,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const ref = React.useRef<HTMLDivElement>(null);\n    React.useImperativeHandle(\n      forwardedRef,\n      () => ref.current as HTMLDivElement,\n    );\n    const reduceMotion = useReducedMotion();\n\n    // Pointer normalized to -0.5..0.5 over the card, spring-smoothed.\n    const px = useMotionValue(0);\n    const py = useMotionValue(0);\n    const sx = useSpring(px, SPRING);\n    const sy = useSpring(py, SPRING);\n\n    const rotateX = useTransform(sy, [-0.5, 0.5], [maxTilt, -maxTilt]);\n    const rotateY = useTransform(sx, [-0.5, 0.5], [-maxTilt, maxTilt]);\n    const holoX = useTransform(sx, [-0.5, 0.5], [\"0%\", \"100%\"]);\n    const holoY = useTransform(sy, [-0.5, 0.5], [\"0%\", \"100%\"]);\n\n    // Optional gyroscope tilt for touch devices (opt-in). iOS 13+ gates the\n    // sensor behind a permission prompt that must fire from a user gesture, so\n    // we request it on the first tap, then map beta/gamma onto the same values.\n    React.useEffect(() => {\n      if (!gyroscope || reduceMotion) return;\n      if (\n        typeof window === \"undefined\" ||\n        !(\"DeviceOrientationEvent\" in window)\n      )\n        return;\n\n      let attached = false;\n      const handleOrientation = (e: DeviceOrientationEvent) => {\n        if (e.gamma == null || e.beta == null) return;\n        // gamma: left/right (-45..45), beta: front/back (0..90 upright).\n        px.set(Math.max(-0.5, Math.min(0.5, e.gamma / 45)));\n        py.set(Math.max(-0.5, Math.min(0.5, (e.beta - 45) / 45)));\n      };\n      const attach = () => {\n        if (attached) return;\n        attached = true;\n        window.addEventListener(\"deviceorientation\", handleOrientation);\n      };\n\n      const orientationEvent = window.DeviceOrientationEvent as unknown as {\n        requestPermission?: () => Promise<\"granted\" | \"denied\">;\n      };\n      const requestOnTap = () => {\n        orientationEvent\n          .requestPermission?.()\n          .then((state) => {\n            if (state === \"granted\") attach();\n          })\n          .catch(() => {});\n        window.removeEventListener(\"pointerdown\", requestOnTap);\n      };\n\n      if (typeof orientationEvent.requestPermission === \"function\") {\n        window.addEventListener(\"pointerdown\", requestOnTap);\n      } else {\n        attach();\n      }\n\n      return () => {\n        window.removeEventListener(\"deviceorientation\", handleOrientation);\n        window.removeEventListener(\"pointerdown\", requestOnTap);\n      };\n    }, [gyroscope, reduceMotion, px, py]);\n\n    const handleMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (!reduceMotion) {\n        const el = ref.current;\n        if (el) {\n          const rect = el.getBoundingClientRect();\n          px.set((e.clientX - rect.left) / rect.width - 0.5);\n          py.set((e.clientY - rect.top) / rect.height - 0.5);\n        }\n      }\n      onPointerMove?.(e);\n    };\n\n    const handleLeave = (e: React.PointerEvent<HTMLDivElement>) => {\n      px.set(0);\n      py.set(0);\n      onPointerLeave?.(e);\n    };\n\n    // Reduced motion: a still card with the foil resting at center — no tilt,\n    // no pointer tracking, no gyroscope.\n    if (reduceMotion) {\n      return (\n        <div\n          ref={ref}\n          data-slot=\"holographic-card\"\n          className={`${ROOT_BASE} ${className ?? \"\"}`}\n          style={\n            {\n              \"--holo-x\": \"50%\",\n              \"--holo-y\": \"40%\",\n              ...style,\n            } as React.CSSProperties\n          }\n          {...props}\n        >\n          <div aria-hidden className={`absolute inset-0 ${BASE[variant]}`} />\n          <div\n            aria-hidden\n            className={`${FOIL_LAYER} ${FOIL[variant]} opacity-50`}\n          />\n          <div className=\"relative z-raised\">{children}</div>\n          <div aria-hidden className={EDGE_LAYER} />\n        </div>\n      );\n    }\n\n    return (\n      // Outer wrapper owns the perspective so the tilt reads as real depth.\n      <div className=\"[perspective:1200px]\">\n        <motion.div\n          ref={ref}\n          data-slot=\"holographic-card\"\n          onPointerMove={handleMove}\n          onPointerLeave={handleLeave}\n          whileHover={{ scale: 1.03 }}\n          style={\n            {\n              rotateX,\n              rotateY,\n              \"--holo-x\": holoX,\n              \"--holo-y\": holoY,\n              ...style,\n            } as React.ComponentProps<typeof motion.div>[\"style\"]\n          }\n          className={`${ROOT_BASE} ${className ?? \"\"}`}\n          {...(props as React.ComponentProps<typeof motion.div>)}\n        >\n          <div aria-hidden className={`absolute inset-0 ${BASE[variant]}`} />\n          <div\n            aria-hidden\n            className={`${FOIL_LAYER} ${FOIL[variant]} opacity-60`}\n          />\n          {sparkle ? <div aria-hidden className={SPARKLE_LAYER} /> : null}\n          <div className=\"relative z-raised\">{children}</div>\n          {glare ? <div aria-hidden className={GLARE_LAYER} /> : null}\n          <div aria-hidden className={EDGE_LAYER} />\n        </motion.div>\n      </div>\n    );\n  },\n);\nHolographicCard.displayName = \"HolographicCard\";\n\nexport { HolographicCard };\n",
      "type": "registry:ui",
      "target": "components/godui/holographic-card.tsx"
    }
  ],
  "type": "registry:ui"
}
