{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voltaic",
  "type": "registry:ui",
  "title": "Voltaic",
  "description": "An electric turbulence border — the card's perimeter is a living arc: an SVG rounded-rect stroke deformed by animated procedural turbulence so it writhes, crackles on seeded reseeds, and periodically misfires and re-strikes, under a soft violet corona. Hover ramps the displacement. Throttled rAF that pauses offscreen; static softly-glowing border under reduced motion.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-reduced-motion.json",
    "https://crucibleui.com/r/use-visibility-pause.json"
  ],
  "files": [
    {
      "path": "registry/default/cards/voltaic/voltaic.tsx",
      "type": "registry:ui",
      "target": "components/crucible/voltaic.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 VoltaicProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /** Card content. */\n  children: React.ReactNode;\n  /** Arc color, any CSS color. The stroke core is auto-mixed toward white. @default electric violet */\n  color?: string;\n  /** Displacement strength — 0 is a calm line, higher is more violent arcing. @default 1 */\n  intensity?: number;\n  /** Temporal churn multiplier for the turbulence. @default 1 */\n  speed?: number;\n  /** Corner radius of the card and its arc, in px. @default 16 */\n  borderRadius?: number;\n  /** Freeze the arc at its current frame. @default false */\n  paused?: boolean;\n}\n\n/** How far the SVG bleeds past the card so displaced kinks aren't clipped. */\nconst PAD = 12;\n\n/** Deterministic PRNG so reseed/misfire schedules are identical every run (SSR-safe, no Math.random). */\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst rand = mulberry32(0x5eed);\nconst SEEDS = Array.from({ length: 32 }, () => Math.floor(rand() * 1000));\nconst MISFIRE_AT = Array.from({ length: 16 }, () => 0.25 + rand() * 0.55);\n\n/**\n * Voltaic — an electric turbulence border. The card's perimeter is a live\n * arc: an SVG rounded-rect stroke continuously deformed by animated\n * procedural turbulence (feTurbulence noise drifting through feOffset, then\n * feDisplacementMap), so the line writhes and — on periodic reseeds —\n * crackles to a new configuration, under a soft violet corona. Every few\n * seconds the turbulence misfires to near-zero for a beat and snaps back,\n * like an arc re-striking. Hover ramps the displacement. Filter attributes\n * are driven by refs in a throttled rAF loop that pauses offscreen and on\n * hidden tabs; all randomness is seeded. Reduced motion: a static,\n * softly-glowing border with zero displacement.\n */\nexport function Voltaic({\n  children,\n  color = \"#a78bfa\",\n  intensity = 1,\n  speed = 1,\n  borderRadius = 16,\n  paused = false,\n  className,\n  style,\n  ...props\n}: VoltaicProps) {\n  const reducedMotion = useReducedMotion();\n  const rawId = React.useId();\n  const id = React.useMemo(() => `voltaic-${rawId.replace(/[^a-zA-Z0-9-]/g, \"\")}`, [rawId]);\n\n  const turbRefs = React.useRef<(SVGFETurbulenceElement | null)[]>([null, null]);\n  const offsetRefs = React.useRef<(SVGFEOffsetElement | null)[]>([null, null]);\n  const displaceRefs = React.useRef<(SVGFEDisplacementMapElement | null)[]>([null, null]);\n  const hoverRef = React.useRef(false);\n  const boostRef = React.useRef(0);\n  const lastFrame = React.useRef(0);\n\n  const nodeRef = useVisibilityPause<HTMLDivElement>(\n    (elapsed) => {\n      // SVG filters re-rasterize on every attribute write — 30fps is plenty.\n      if (elapsed - lastFrame.current < 1 / 30) return;\n      lastFrame.current = elapsed;\n      const t = elapsed * speed;\n\n      boostRef.current += ((hoverRef.current ? 1 : 0) - boostRef.current) * 0.14;\n\n      // Misfire: every cycle the turbulence drops to near-zero for ~150ms at\n      // a seeded point in the cycle, then snaps back — the arc re-striking.\n      const CYCLE = 3.4;\n      const cycleIndex = Math.floor(t / CYCLE);\n      const phase = t / CYCLE - cycleIndex;\n      const misfireAt = MISFIRE_AT[cycleIndex % MISFIRE_AT.length] ?? 0.5;\n      const env = phase > misfireAt && phase < misfireAt + 0.045 ? 0.06 : 1;\n\n      // Cap the displacement so high intensity + hover boost can't push stroke\n      // samples far enough apart to fragment the arc into detached specks that\n      // spill past the filter region (~24 is where the path starts breaking up).\n      const scale = Math.min(13 * intensity * (1 + 0.65 * boostRef.current) * env, 22).toFixed(2);\n      // Bounded scroll of the noise offset. A linear (t * 30) drift grows without\n      // limit and eventually slides the turbulence entirely out of the filter\n      // region — the arc fades away after a minute or two. A slow sine keeps the\n      // pattern travelling seamlessly forever; the crackle comes from reseeding below.\n      const dx = (Math.sin(t * 0.23) * 60).toFixed(2);\n      const dy = (Math.sin(t * 0.8) * 10).toFixed(2);\n      const seed = String(SEEDS[Math.floor(t / 0.55) % SEEDS.length]);\n\n      for (const el of displaceRefs.current) el?.setAttribute(\"scale\", scale);\n      for (const el of offsetRefs.current) {\n        el?.setAttribute(\"dx\", dx);\n        el?.setAttribute(\"dy\", dy);\n      }\n      for (const el of turbRefs.current) {\n        // Reseeding jumps the noise to a new configuration — the crackle.\n        if (el && el.getAttribute(\"seed\") !== seed) el.setAttribute(\"seed\", seed);\n      }\n    },\n    { paused: paused || reducedMotion }\n  );\n\n  // If reduced motion engages mid-session, settle the arc back to a calm line.\n  React.useEffect(() => {\n    if (!reducedMotion) return;\n    for (const el of displaceRefs.current) el?.setAttribute(\"scale\", \"0\");\n    boostRef.current = 0;\n  }, [reducedMotion]);\n\n  const handlePointerEnter = React.useCallback(() => {\n    hoverRef.current = true;\n  }, []);\n  const handlePointerLeave = React.useCallback(() => {\n    hoverRef.current = false;\n  }, []);\n\n  const rectGeometry: React.CSSProperties = {\n    [\"x\" as string]: `${PAD}px`,\n    [\"y\" as string]: `${PAD}px`,\n    [\"width\" as string]: `calc(100% - ${PAD * 2}px)`,\n    [\"height\" as string]: `calc(100% - ${PAD * 2}px)`,\n  };\n\n  const turbulence = (index: number, result: string) => (\n    <>\n      <feTurbulence\n        ref={(el) => {\n          turbRefs.current[index] = el;\n        }}\n        type=\"turbulence\"\n        baseFrequency=\"0.02 0.06\"\n        numOctaves={2}\n        seed={7}\n        result={`${result}-noise`}\n      />\n      <feOffset\n        ref={(el) => {\n          offsetRefs.current[index] = el;\n        }}\n        in={`${result}-noise`}\n        dx={0}\n        dy={0}\n        result={`${result}-drift`}\n      />\n      <feDisplacementMap\n        ref={(el) => {\n          displaceRefs.current[index] = el;\n        }}\n        in=\"SourceGraphic\"\n        in2={`${result}-drift`}\n        scale={0}\n        xChannelSelector=\"R\"\n        yChannelSelector=\"G\"\n        result={`${result}-arc`}\n      />\n    </>\n  );\n\n  return (\n    <div\n      ref={nodeRef}\n      data-crucible=\"voltaic\"\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      className={cn(\"relative border border-white/10 bg-neutral-900\", className)}\n      style={{\n        borderRadius,\n        boxShadow: `0 0 36px -10px color-mix(in oklab, ${color} 45%, transparent)`,\n        ...style,\n      }}\n      {...props}\n    >\n      <div className=\"relative rounded-[inherit]\">{children}</div>\n      <svg\n        aria-hidden\n        className=\"pointer-events-none absolute overflow-visible\"\n        style={{\n          left: -PAD,\n          top: -PAD,\n          width: `calc(100% + ${PAD * 2}px)`,\n          height: `calc(100% + ${PAD * 2}px)`,\n        }}\n      >\n        <defs>\n          <filter id={`${id}-arc`} x=\"-25%\" y=\"-25%\" width=\"150%\" height=\"150%\">\n            {turbulence(0, \"a\")}\n          </filter>\n          <filter id={`${id}-corona`} x=\"-25%\" y=\"-25%\" width=\"150%\" height=\"150%\">\n            {turbulence(1, \"b\")}\n            <feGaussianBlur in=\"b-arc\" stdDeviation=\"3.5\" />\n          </filter>\n        </defs>\n        {/* Violet corona: the same displaced path, blurred wide. */}\n        <rect\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={2.5}\n          opacity={0.85}\n          rx={borderRadius}\n          filter={`url(#${id}-corona)`}\n          style={rectGeometry}\n        />\n        {/* Pale core: near-white filament riding the identical turbulence. */}\n        <rect\n          fill=\"none\"\n          strokeWidth={1.2}\n          rx={borderRadius}\n          filter={`url(#${id}-arc)`}\n          style={{\n            ...rectGeometry,\n            stroke: `color-mix(in oklab, ${color} 30%, white)`,\n          }}\n        />\n      </svg>\n    </div>\n  );\n}\n"
    }
  ]
}