{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bevel",
  "type": "registry:ui",
  "title": "Bevel",
  "description": "A glass-refraction surface card: backdrop-blurred glass with a multi-layer bevel edge, a slow glacial-cyan caustic drifting along the top rim, and a refracted offset highlight that slides with a subtle perspective tilt on hover. Pure CSS with eased custom-property transitions; static caustics and no tilt under reduced motion; a ghost light idles across the glass 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/bevel/bevel.tsx",
      "type": "registry:ui",
      "target": "components/crucible/bevel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface BevelProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Card content. */\n  children: React.ReactNode;\n  /** Maximum tilt, in degrees, on either axis at the card's edge. Subtle by design. @default 6 */\n  tilt?: number;\n  /** Color of the caustic light on the bevel edges and the internal refracted highlight. @default glacial cyan */\n  causticColor?: string;\n  /** Backdrop blur in px — how strongly the glass frosts whatever sits behind it. @default 16 */\n  blur?: number;\n  /**\n   * Idle autoplay: after 1.5s with no pointer on the card, a slow ghost\n   * light drifts across it — a gentle tilt and sliding caustic at reduced\n   * strength — so the glass never reads as dead. A real pointer takes over\n   * instantly. Off under reduced motion; sleeps offscreen. @default true\n   */\n  idle?: boolean;\n}\n\nconst IDLE_MS = 1500;\n/** Ghost strength relative to a real pointer — deliberate, never a full hover. */\nconst GHOST_GAIN = 0.55;\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 * Bevel — a glass-refraction surface card. A glassmorphic panel (backdrop\n * blur + saturation over whatever sits behind it) with a multi-layer edge:\n * an outer hairline, a soft white inner bevel, and a slow glacial-cyan\n * caustic that drifts along the top edge. On hover the panel tilts gently in\n * perspective and a refracted offset highlight — plus a thin caustic line on\n * the lower-inside bevel — slides with the pointer, like light cast through\n * thick glass. Tilt eases via registered custom-property transitions (pure\n * CSS, no animation library) and disables for touch input. Reduced motion:\n * static caustics at a handsome phase, no tilt, no drift. Untouched, a ghost\n * light idles across the pane (`idle`), handing over to the real pointer.\n */\nexport function Bevel({\n  children,\n  tilt = 6,\n  causticColor = \"#7dd3fc\",\n  blur = 16,\n  idle = true,\n  className,\n  style,\n  ...props\n}: BevelProps) {\n  const reducedMotion = useReducedMotion();\n  const hoverRef = React.useRef(false);\n  const lastPointerRef = React.useRef(0);\n  const ghostRef = React.useRef({ presence: 0, last: 0, on: false });\n  const phase = idPhase(React.useId());\n\n  // Ghost driver: a slow Lissajous light path, eased in so it never snaps.\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) return;\n      if (elapsed < g.last) g.last = elapsed;\n      const dt = Math.min(elapsed - g.last, 0.05);\n      g.last = elapsed;\n      if (hoverRef.current) {\n        g.presence = 0;\n        g.on = false;\n        return;\n      }\n      const want = performance.now() - lastPointerRef.current > IDLE_MS ? 1 : 0;\n      g.presence += (want - g.presence) * Math.min(1, dt * 1.6);\n      if (g.presence < 0.01 && !want) {\n        if (g.on) {\n          g.on = false;\n          node.setAttribute(\"data-hover\", \"false\");\n        }\n        return;\n      }\n      if (!g.on) {\n        g.on = true;\n        node.setAttribute(\"data-hover\", \"ghost\");\n      }\n      const px = 0.5 + 0.34 * Math.sin(elapsed * 0.41 + phase + 0.7);\n      const py = 0.45 + 0.3 * Math.sin(elapsed * 0.29 + 2.1 + phase * 1.7);\n      const k = g.presence * GHOST_GAIN;\n      node.style.setProperty(\"--bevel-rx\", `${(-(py - 0.5) * 2 * tilt * k).toFixed(3)}deg`);\n      node.style.setProperty(\"--bevel-ry\", `${((px - 0.5) * 2 * tilt * k).toFixed(3)}deg`);\n      node.style.setProperty(\"--bevel-px\", `${((0.5 + (px - 0.5) * g.presence) * 100).toFixed(2)}%`);\n      node.style.setProperty(\"--bevel-py\", `${((0.4 + (py - 0.4) * g.presence) * 100).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 its first pass).\n  React.useEffect(() => {\n    lastPointerRef.current = performance.now();\n  }, []);\n\n  const handlePointerMove = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      if (reducedMotion || event.pointerType === \"touch\") return;\n      const node = nodeRef.current;\n      if (!node) return;\n      hoverRef.current = true;\n      lastPointerRef.current = performance.now();\n      if (node.getAttribute(\"data-hover\") !== \"true\") node.setAttribute(\"data-hover\", \"true\");\n      const rect = node.getBoundingClientRect();\n      const px = (event.clientX - rect.left) / rect.width;\n      const py = (event.clientY - rect.top) / rect.height;\n      node.style.setProperty(\"--bevel-rx\", `${(-(py - 0.5) * 2 * tilt).toFixed(3)}deg`);\n      node.style.setProperty(\"--bevel-ry\", `${((px - 0.5) * 2 * tilt).toFixed(3)}deg`);\n      node.style.setProperty(\"--bevel-px\", `${(px * 100).toFixed(2)}%`);\n      node.style.setProperty(\"--bevel-py\", `${(py * 100).toFixed(2)}%`);\n    },\n    [reducedMotion, tilt, nodeRef]\n  );\n\n  const handlePointerEnter = React.useCallback(\n    (event: React.PointerEvent<HTMLDivElement>) => {\n      if (reducedMotion || event.pointerType === \"touch\") return;\n      hoverRef.current = true;\n      lastPointerRef.current = performance.now();\n      ghostRef.current.presence = 0;\n      ghostRef.current.on = false;\n      nodeRef.current?.setAttribute(\"data-hover\", \"true\");\n    },\n    [reducedMotion, nodeRef]\n  );\n\n  const handlePointerLeave = React.useCallback(() => {\n    hoverRef.current = false;\n    lastPointerRef.current = performance.now();\n    const node = nodeRef.current;\n    if (!node) return;\n    node.setAttribute(\"data-hover\", \"false\");\n    node.style.setProperty(\"--bevel-rx\", \"0deg\");\n    node.style.setProperty(\"--bevel-ry\", \"0deg\");\n    node.style.setProperty(\"--bevel-px\", \"50%\");\n    node.style.setProperty(\"--bevel-py\", \"40%\");\n  }, [nodeRef]);\n\n  return (\n    <div\n      ref={nodeRef}\n      data-crucible=\"bevel\"\n      data-hover=\"false\"\n      data-reduced-motion={reducedMotion}\n      onPointerMove={handlePointerMove}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      className={cn(\n        \"relative overflow-hidden rounded-2xl border border-white/10\",\n        \"focus-within:border-white/25\",\n        className\n      )}\n      style={{\n        [\"--bevel-rx\" as string]: \"0deg\",\n        [\"--bevel-ry\" as string]: \"0deg\",\n        [\"--bevel-px\" as string]: \"50%\",\n        [\"--bevel-py\" as string]: \"40%\",\n        [\"--bevel-caustic\" as string]: causticColor,\n        backgroundColor: \"rgba(18, 21, 27, 0.32)\",\n        backdropFilter: `blur(${blur}px) saturate(150%)`,\n        WebkitBackdropFilter: `blur(${blur}px) saturate(150%)`,\n        ...style,\n      }}\n      {...props}\n    >\n      <style>{`\n        @property --bevel-rx {\n          syntax: \"<angle>\";\n          inherits: true;\n          initial-value: 0deg;\n        }\n        @property --bevel-ry {\n          syntax: \"<angle>\";\n          inherits: true;\n          initial-value: 0deg;\n        }\n        @property --bevel-px {\n          syntax: \"<percentage>\";\n          inherits: true;\n          initial-value: 50%;\n        }\n        @property --bevel-py {\n          syntax: \"<percentage>\";\n          inherits: true;\n          initial-value: 40%;\n        }\n        [data-crucible=\"bevel\"] {\n          isolation: isolate;\n          transform: perspective(1100px) rotateX(var(--bevel-rx)) rotateY(var(--bevel-ry));\n          /* Registered custom properties interpolate, so the tilt and the\n             refracted light trail the pointer with a heavy, glassy lag\n             instead of snapping. Unregistered fallback follows instantly. */\n          transition:\n            --bevel-rx 260ms cubic-bezier(0.22, 1, 0.36, 1),\n            --bevel-ry 260ms cubic-bezier(0.22, 1, 0.36, 1),\n            --bevel-px 260ms cubic-bezier(0.22, 1, 0.36, 1),\n            --bevel-py 260ms cubic-bezier(0.22, 1, 0.36, 1);\n        }\n        [data-crucible=\"bevel\"] .bevel-layer {\n          position: absolute;\n          inset: 0;\n          border-radius: inherit;\n          pointer-events: none;\n        }\n        /* Inner bevel: soft white top edge, faint bottom return, hairline\n           inner ring — the three-line stack that reads as ground glass. */\n        [data-crucible=\"bevel\"] .bevel-edge {\n          box-shadow:\n            inset 0 1px 0 rgba(255, 255, 255, 0.22),\n            inset 0 2px 6px -2px rgba(255, 255, 255, 0.1),\n            inset 0 -1px 0 rgba(255, 255, 255, 0.05),\n            inset 0 0 0 1px rgba(255, 255, 255, 0.04);\n        }\n        /* Caustic sheen drifting along the top bevel. */\n        [data-crucible=\"bevel\"] .bevel-caustic-top {\n          inset: 0 0 auto 0;\n          height: 2px;\n          border-radius: 0;\n          background: linear-gradient(\n            90deg,\n            transparent,\n            color-mix(in oklab, var(--bevel-caustic) 55%, white) 45%,\n            color-mix(in oklab, var(--bevel-caustic) 85%, transparent) 55%,\n            transparent\n          );\n          background-size: 55% 100%;\n          background-repeat: no-repeat;\n          background-position: 35% 0;\n          filter: drop-shadow(0 0 6px color-mix(in oklab, var(--bevel-caustic) 60%, transparent));\n          opacity: 0.7;\n          animation: bevel-drift 9s ease-in-out infinite alternate;\n          transition: opacity 400ms ease;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"true\"] .bevel-caustic-top {\n          opacity: 1;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"ghost\"] .bevel-caustic-top {\n          opacity: 0.85;\n        }\n        /* The refracted line on the lower-inside bevel — slides with the\n           pointer, like a caustic cast through the pane onto its far edge. */\n        [data-crucible=\"bevel\"] .bevel-caustic-low {\n          inset: auto 0 0 0;\n          height: 1.5px;\n          border-radius: 0;\n          background: linear-gradient(\n            90deg,\n            transparent,\n            color-mix(in oklab, var(--bevel-caustic) 70%, transparent),\n            transparent\n          );\n          background-size: 45% 100%;\n          background-repeat: no-repeat;\n          background-position: var(--bevel-px) 0;\n          opacity: 0;\n          transition: opacity 400ms ease;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"true\"] .bevel-caustic-low {\n          opacity: 0.8;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"ghost\"] .bevel-caustic-low {\n          opacity: 0.5;\n        }\n        /* Internal refracted highlight: offset from the pointer (light bends\n           through the glass, so it never sits directly under the cursor). */\n        [data-crucible=\"bevel\"] .bevel-refract {\n          background: radial-gradient(\n            130% 90% at calc(var(--bevel-px) + 14%) calc(var(--bevel-py) - 22%),\n            color-mix(in oklab, var(--bevel-caustic) 12%, transparent),\n            color-mix(in oklab, var(--bevel-caustic) 5%, transparent) 40%,\n            transparent 68%\n          );\n          mix-blend-mode: screen;\n          opacity: 0.55;\n          transition: opacity 400ms ease;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"true\"] .bevel-refract {\n          opacity: 0.95;\n        }\n        [data-crucible=\"bevel\"][data-hover=\"ghost\"] .bevel-refract {\n          opacity: 0.75;\n        }\n        @keyframes bevel-drift {\n          from {\n            background-position: -12% 0;\n          }\n          to {\n            background-position: 112% 0;\n          }\n        }\n        [data-crucible=\"bevel\"][data-reduced-motion=\"true\"] {\n          transform: none;\n          transition: none;\n        }\n        [data-crucible=\"bevel\"][data-reduced-motion=\"true\"] .bevel-caustic-top {\n          animation: none;\n          background-position: 35% 0;\n          opacity: 0.7;\n        }\n        [data-crucible=\"bevel\"][data-reduced-motion=\"true\"] .bevel-caustic-low {\n          opacity: 0.5;\n        }\n        [data-crucible=\"bevel\"][data-reduced-motion=\"true\"] .bevel-refract {\n          opacity: 0.45;\n          transition: none;\n        }\n        @media (prefers-reduced-motion: reduce) {\n          [data-crucible=\"bevel\"] .bevel-caustic-top {\n            animation: none;\n          }\n        }\n      `}</style>\n      <div className=\"relative z-10\">{children}</div>\n      <div className=\"bevel-layer bevel-refract\" aria-hidden />\n      <div className=\"bevel-layer bevel-edge\" aria-hidden />\n      <div className=\"bevel-layer bevel-caustic-top\" aria-hidden />\n      <div className=\"bevel-layer bevel-caustic-low\" aria-hidden />\n    </div>\n  );\n}\n"
    }
  ]
}