{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kindle",
  "type": "registry:ui",
  "title": "Kindle",
  "description": "A cursor-following radial spotlight card — a soft white glow that breathes gently and brightens the nearest border edge as it passes. Pure CSS, works with arbitrary children; a dimmed ghost spotlight wanders the card when untouched.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-visibility-pause.json"
  ],
  "files": [
    {
      "path": "registry/default/cards/kindle/kindle.tsx",
      "type": "registry:ui",
      "target": "components/crucible/kindle.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface KindleProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Arbitrary card content. */\n  children: React.ReactNode;\n  /** Core color of the spotlight, any CSS color. @default soft white */\n  color?: string;\n  /** Outer rim / haze color, any CSS color. @default faint white */\n  rimColor?: string;\n  /** Spotlight radius in px. @default 240 */\n  radius?: number;\n  /** Brighten the border segment nearest the cursor. @default true */\n  borderGlow?: boolean;\n  /**\n   * Idle autoplay: after 1.5s with no pointer on the card, a dimmed ghost\n   * spotlight wanders the face on a slow Lissajous path. A real pointer takes\n   * over instantly. Off under reduced motion; sleeps offscreen. @default true\n   */\n  idle?: boolean;\n}\n\nconst IDLE_MS = 1500;\n\n/** Stable per-instance phase from React's id, so sibling cards never ghost in lockstep. */\nfunction idPhase(id: string): number {\n  let h = 2166136261;\n  for (let i = 0; i < id.length; i++) h = Math.imul(h ^ id.charCodeAt(i), 16777619);\n  return ((h >>> 0) / 4294967296) * Math.PI * 2;\n}\n\n/**\n * Kindle — a cursor-following radial spotlight card. A hot white core rides\n * inside a wider signature-hue halo and a faint conic sheen; the light tracks\n * the pointer with an eased lag and the nearest border segment blooms under\n * it. At rest the card is alive — a top-lit machined bevel, a slow breathing\n * glow, and a sheen that drifts across the face — so it never reads as a dead\n * slab. Low-opacity grain kills banding for a shader-grade surface.\n * Pure CSS + a lightweight pointer listener; works with arbitrary children.\n * Untouched, a dimmed ghost spotlight wanders the face (`idle`) until a real\n * pointer takes over. Freezes to a designed, legible static glow under\n * `prefers-reduced-motion`.\n */\nexport function Kindle({\n  children,\n  className,\n  style,\n  color = \"rgba(255,255,255,0.55)\",\n  rimColor = \"rgba(255,255,255,0.3)\",\n  radius = 240,\n  borderGlow = true,\n  idle = true,\n  ...props\n}: KindleProps) {\n  const reducedMotion = useReducedMotion();\n  const hoverRef = React.useRef(false);\n  const lastPointerRef = React.useRef(0);\n  const ghostRef = React.useRef({ on: false, since: 0 });\n  const phase = idPhase(React.useId());\n\n  // Ghost driver: positions the spotlight on a slow Lissajous path. Its\n  // presence fades in/out through CSS opacity transitions on data-hover=\"ghost\".\n  // The observed element, mirrored for the rAF tick (set after commit).\n  const elRef = React.useRef<HTMLElement | null>(null);\n  const nodeRef = useVisibilityPause<HTMLDivElement>(\n    (elapsed) => {\n      const node = elRef.current;\n      const g = ghostRef.current;\n      if (!node || hoverRef.current) return;\n      const want = performance.now() - lastPointerRef.current > IDLE_MS;\n      if (want !== g.on) {\n        g.on = want;\n        node.setAttribute(\"data-hover\", want ? \"ghost\" : \"false\");\n      }\n      if (!want) return;\n      const x = 50 + 34 * Math.sin(elapsed * 0.37 + phase);\n      const y = 42 + 30 * Math.sin(elapsed * 0.26 + phase * 1.7 + 1.3);\n      node.style.setProperty(\"--kindle-x\", `${x.toFixed(2)}%`);\n      node.style.setProperty(\"--kindle-y\", `${y.toFixed(2)}%`);\n    },\n    { paused: !idle || reducedMotion }\n  );\n\n  React.useEffect(() => {\n    elRef.current = nodeRef.current;\n  });\n\n  // Arm the idle clock at mount (the ghost waits 1.5s before appearing).\n  React.useEffect(() => {\n    lastPointerRef.current = performance.now();\n  }, []);\n\n  const handlePointerMove = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      if (reducedMotion) return;\n      const node = nodeRef.current;\n      if (!node) return;\n      lastPointerRef.current = performance.now();\n      if (!hoverRef.current) {\n        hoverRef.current = true;\n        ghostRef.current.on = false;\n        node.setAttribute(\"data-hover\", \"true\");\n      }\n      const rect = node.getBoundingClientRect();\n      const x = ((event.clientX - rect.left) / rect.width) * 100;\n      const y = ((event.clientY - rect.top) / rect.height) * 100;\n      node.style.setProperty(\"--kindle-x\", `${x.toFixed(2)}%`);\n      node.style.setProperty(\"--kindle-y\", `${y.toFixed(2)}%`);\n    },\n    [reducedMotion, nodeRef]\n  );\n\n  const handlePointerEnter = React.useCallback(() => {\n    if (reducedMotion) return;\n    hoverRef.current = true;\n    ghostRef.current.on = false;\n    lastPointerRef.current = performance.now();\n    nodeRef.current?.setAttribute(\"data-hover\", \"true\");\n  }, [reducedMotion, nodeRef]);\n\n  const handlePointerLeave = React.useCallback(() => {\n    hoverRef.current = false;\n    ghostRef.current.on = false;\n    lastPointerRef.current = performance.now();\n    nodeRef.current?.setAttribute(\"data-hover\", \"false\");\n  }, [nodeRef]);\n\n  return (\n    <div\n      ref={nodeRef}\n      data-crucible=\"kindle\"\n      data-hover=\"false\"\n      data-reduced-motion={reducedMotion}\n      onPointerMove={handlePointerMove}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      className={cn(\n        \"group relative overflow-hidden rounded-2xl border border-white/10 bg-neutral-900\",\n        className\n      )}\n      style={{\n        [\"--kindle-color\" as string]: color,\n        [\"--kindle-rim\" as string]: rimColor,\n        [\"--kindle-radius\" as string]: `${radius}px`,\n        [\"--kindle-haze-radius\" as string]: `${radius * 1.5}px`,\n        [\"--kindle-x\" as string]: \"50%\",\n        [\"--kindle-y\" as string]: \"30%\",\n        ...style,\n      }}\n      {...props}\n    >\n      <style>{`\n        @property --kindle-x {\n          syntax: \"<percentage>\";\n          inherits: true;\n          initial-value: 50%;\n        }\n        @property --kindle-y {\n          syntax: \"<percentage>\";\n          inherits: true;\n          initial-value: 30%;\n        }\n        [data-crucible=\"kindle\"] {\n          isolation: isolate;\n          /* Registered custom properties interpolate, so the glow trails the\n             pointer with a soft eased lag instead of snapping per-frame.\n             Unregistered fallback (older engines) just follows instantly. */\n          transition:\n            --kindle-x 240ms cubic-bezier(0.22, 1, 0.36, 1),\n            --kindle-y 240ms cubic-bezier(0.22, 1, 0.36, 1);\n        }\n        [data-crucible=\"kindle\"] .kindle-grain,\n        [data-crucible=\"kindle\"] .kindle-bevel,\n        [data-crucible=\"kindle\"] .kindle-rest,\n        [data-crucible=\"kindle\"] .kindle-sheen,\n        [data-crucible=\"kindle\"] .kindle-core,\n        [data-crucible=\"kindle\"] .kindle-haze,\n        [data-crucible=\"kindle\"] .kindle-border {\n          position: absolute;\n          inset: 0;\n          border-radius: inherit;\n          pointer-events: none;\n        }\n\n        /* Machined depth: top-lit inner bevel (bright top return, dark bottom),\n           a hairline inner ring, and a soft inner vignette that seats the\n           surface. Always on — this is the card's material, not an effect. */\n        [data-crucible=\"kindle\"] .kindle-bevel {\n          box-shadow:\n            inset 0 1px 0 color-mix(in oklab, var(--kindle-rim) 55%, transparent),\n            inset 0 2px 10px -6px color-mix(in oklab, var(--kindle-rim) 60%, transparent),\n            inset 0 -1px 0 rgba(0, 0, 0, 0.55),\n            inset 0 -18px 34px -22px rgba(0, 0, 0, 0.7),\n            inset 0 0 0 1px rgba(255, 255, 255, 0.03);\n          background: radial-gradient(\n            135% 120% at 50% -20%,\n            transparent 55%,\n            rgba(0, 0, 0, 0.28) 100%\n          );\n        }\n\n        /* Fine grain over the surface (under the content) — kills gradient\n           banding and gives the light a shader-like, non-flat read. */\n        [data-crucible=\"kindle\"] .kindle-grain {\n          background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23k)'/%3E%3C/svg%3E\");\n          opacity: 0.05;\n          mix-blend-mode: overlay;\n        }\n\n        /* Idle breathing glow: a soft top-lit haze that swells and recedes so\n           the card has life at rest. Signature hue, slow. */\n        [data-crucible=\"kindle\"] .kindle-rest {\n          opacity: 1;\n          transform-origin: 50% -10%;\n          background:\n            radial-gradient(\n              120% 80% at 50% -18%,\n              color-mix(in oklab, var(--kindle-rim) 34%, transparent),\n              color-mix(in oklab, var(--kindle-rim) 10%, transparent) 46%,\n              transparent 70%\n            ),\n            radial-gradient(\n              70% 50% at 50% 118%,\n              color-mix(in oklab, var(--kindle-color) 12%, transparent),\n              transparent 72%\n            );\n          animation: kindle-breathe 6400ms ease-in-out infinite;\n          transition: opacity 400ms ease;\n        }\n        [data-crucible=\"kindle\"][data-hover=\"true\"] .kindle-rest {\n          opacity: 0.5;\n        }\n        [data-crucible=\"kindle\"][data-hover=\"ghost\"] .kindle-rest {\n          opacity: 0.75;\n        }\n\n        /* Drifting sheen: a wide diagonal light bar that slides slowly across\n           the face — a second, independent thread of idle motion. */\n        [data-crucible=\"kindle\"] .kindle-sheen {\n          opacity: 0.55;\n          background: linear-gradient(\n            114deg,\n            transparent 38%,\n            color-mix(in oklab, var(--kindle-rim) 16%, transparent) 50%,\n            transparent 62%\n          );\n          background-size: 230% 100%;\n          background-repeat: no-repeat;\n          background-position: -20% 0;\n          mix-blend-mode: screen;\n          animation: kindle-sheen-drift 12s ease-in-out infinite alternate;\n          transition: opacity 400ms ease;\n        }\n        [data-crucible=\"kindle\"][data-hover=\"true\"] .kindle-sheen {\n          opacity: 0.28;\n        }\n\n        /* Wide signature-hue halo with a faint conic sheen woven in — the\n           volumetric outer glow that the hot core rides inside of. */\n        [data-crucible=\"kindle\"] .kindle-haze {\n          opacity: 0;\n          background:\n            conic-gradient(\n              from 130deg at var(--kindle-x) var(--kindle-y),\n              transparent,\n              color-mix(in oklab, var(--kindle-rim) 10%, transparent) 22%,\n              transparent 46%,\n              color-mix(in oklab, var(--kindle-rim) 8%, transparent) 72%,\n              transparent\n            ),\n            radial-gradient(\n              var(--kindle-haze-radius) circle at var(--kindle-x) var(--kindle-y),\n              color-mix(in oklab, var(--kindle-rim) 34%, transparent),\n              color-mix(in oklab, var(--kindle-rim) 14%, transparent) 45%,\n              transparent 78%\n            );\n          mix-blend-mode: screen;\n          transform-origin: var(--kindle-x) var(--kindle-y);\n          animation: kindle-pulse 3200ms ease-in-out infinite;\n          transition: opacity 350ms ease;\n        }\n\n        /* Tight hot core: white-hot center melting into the signature hue —\n           the two-layer read is what makes the light feel volumetric. */\n        [data-crucible=\"kindle\"] .kindle-core {\n          opacity: 0;\n          background: radial-gradient(\n            calc(var(--kindle-radius) * 0.58) circle at var(--kindle-x) var(--kindle-y),\n            color-mix(in oklab, var(--kindle-color) 72%, white),\n            color-mix(in oklab, var(--kindle-color) 52%, transparent) 30%,\n            color-mix(in oklab, var(--kindle-color) 22%, transparent) 58%,\n            transparent 76%\n          );\n          mix-blend-mode: screen;\n          transition: opacity 350ms ease;\n        }\n\n        /* Border bloom: a 1px gradient ring that brightens — and softly\n           blooms — on the edge segment nearest the cursor. */\n        [data-crucible=\"kindle\"] .kindle-border {\n          opacity: 0;\n          padding: 1px;\n          background: radial-gradient(\n            calc(var(--kindle-radius) * 0.95) circle at var(--kindle-x) var(--kindle-y),\n            color-mix(in oklab, var(--kindle-color) 40%, white),\n            color-mix(in oklab, var(--kindle-color) 75%, transparent) 32%,\n            transparent 64%\n          );\n          filter: drop-shadow(0 0 5px color-mix(in oklab, var(--kindle-color) 45%, transparent));\n          -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);\n          -webkit-mask-composite: xor;\n          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);\n          mask-composite: exclude;\n          transition: opacity 350ms ease;\n        }\n\n        [data-crucible=\"kindle\"][data-hover=\"true\"] .kindle-core,\n        [data-crucible=\"kindle\"][data-hover=\"true\"] .kindle-haze,\n        [data-crucible=\"kindle\"][data-hover=\"true\"] .kindle-border {\n          opacity: 1;\n        }\n        /* Ghost spotlight: a deliberate, dimmer pass — never a full hover. Slow\n           fades so its arrival and departure read as light, not a toggle. */\n        [data-crucible=\"kindle\"][data-hover=\"ghost\"] .kindle-core,\n        [data-crucible=\"kindle\"][data-hover=\"ghost\"] .kindle-haze,\n        [data-crucible=\"kindle\"][data-hover=\"ghost\"] .kindle-border {\n          opacity: 0.5;\n          transition-duration: 900ms;\n        }\n        [data-crucible=\"kindle\"][data-hover=\"ghost\"] {\n          transition:\n            --kindle-x 600ms linear,\n            --kindle-y 600ms linear;\n        }\n\n        @keyframes kindle-breathe {\n          0%, 100% { opacity: 0.82; transform: scale(1); }\n          50% { opacity: 1; transform: scale(1.04); }\n        }\n        @keyframes kindle-pulse {\n          0%, 100% { transform: scale(1); }\n          50% { transform: scale(1.06); }\n        }\n        @keyframes kindle-sheen-drift {\n          from { background-position: -25% 0; }\n          to { background-position: 125% 0; }\n        }\n\n        /* Reduced motion: a designed, legible static frame — machined bevel,\n           grain, a still resting glow, a fixed sheen, and a soft centered\n           spotlight pinned at the default position. Never blank, never frozen\n           mid-animation. */\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] {\n          transition: none;\n        }\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] .kindle-rest {\n          opacity: 1;\n          transform: scale(1);\n          animation: none;\n          transition: none;\n        }\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] .kindle-sheen {\n          opacity: 0.4;\n          background-position: 62% 0;\n          animation: none;\n          transition: none;\n        }\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] .kindle-core {\n          opacity: 0.42;\n          animation: none;\n          transition: none;\n        }\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] .kindle-haze {\n          opacity: 0.32;\n          transform: scale(1);\n          animation: none;\n          transition: none;\n        }\n        [data-crucible=\"kindle\"][data-reduced-motion=\"true\"] .kindle-border {\n          opacity: 0.5;\n          transition: none;\n        }\n        @media (prefers-reduced-motion: reduce) {\n          [data-crucible=\"kindle\"] .kindle-rest,\n          [data-crucible=\"kindle\"] .kindle-sheen,\n          [data-crucible=\"kindle\"] .kindle-haze {\n            animation: none;\n          }\n        }\n      `}</style>\n      <div className=\"kindle-grain\" aria-hidden />\n      <div className=\"kindle-bevel\" aria-hidden />\n      <div className=\"kindle-rest\" aria-hidden />\n      <div className=\"kindle-sheen\" aria-hidden />\n      <div className=\"kindle-haze\" aria-hidden />\n      <div className=\"kindle-core\" aria-hidden />\n      {borderGlow && <div className=\"kindle-border\" aria-hidden />}\n      <div className=\"relative z-10\">{children}</div>\n    </div>\n  );\n}\n"
    }
  ]
}