{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gradient-border-beam","type":"registry:component","title":"Animated Gradient Border Beam","description":"A comet of light that circles a card's border at an even pace: a faded tail behind a white-hot head, glow that bleeds onto the surface and past the edge, an optional second beam the other way, and a pointer light on hover. Wraps any content. No dependencies.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/gradient-border-beam.tsx","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\n\n/** A comet of light that circles a card's border at an even pace, with its glow\n *  bleeding onto the surface and just outside the edge. Wraps any children. */\nexport function BorderBeamCard({\n  label = \"Border beam\",\n  speed = 4,\n  children,\n  color = \"#ff2d2d\",\n  coreColor = \"#ffffff\",\n  size = 22,\n  borderWidth = 2,\n  radius = 16,\n  glow = 1,\n  dual = false,\n  reverse = false,\n  surface = \"#0b0c0f\",\n  className = \"\",\n}: {\n  label?: string;\n  /** Seconds per lap. */\n  speed?: number;\n  children?: ReactNode;\n  /** Tail colour of the beam. */\n  color?: string;\n  /** Hot colour at the head of the beam. */\n  coreColor?: string;\n  /** Beam length as a percentage of the perimeter. */\n  size?: number;\n  borderWidth?: number;\n  /** Corner radius in px. */\n  radius?: number;\n  /** Glow strength, 0 to 2. */\n  glow?: number;\n  /** A second beam travelling the other way. */\n  dual?: boolean;\n  reverse?: boolean;\n  /** Card surface colour. */\n  surface?: string;\n  className?: string;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const hasChildren = children !== undefined && children !== null && children !== false;\n  const safe = (c: string, fallback: string) => (/^[#a-zA-Z0-9(),.%\\s/-]{1,48}$/.test(c) ? c : fallback);\n  const tail = safe(color, \"#ff2d2d\");\n  const core = safe(coreColor, \"#ffffff\");\n  const bg = safe(surface, \"#0b0c0f\");\n  const bw = Math.min(8, Math.max(1, borderWidth));\n  const r = Math.max(0, radius);\n  const len = Math.min(60, Math.max(4, size));\n  const lap = Math.max(0.5, speed);\n  const g = Math.min(2, Math.max(0, glow));\n\n  // Sleep the animation while off screen.\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    if (typeof IntersectionObserver === \"undefined\") {\n      el.dataset.live = \"1\";\n      return;\n    }\n    const io = new IntersectionObserver(\n      (entries) => {\n        for (const e of entries) el.dataset.live = e.isIntersecting ? \"1\" : \"0\";\n      },\n      { rootMargin: \"120px\" },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  // Pointer light on the surface, in layout px (previews may be scaled).\n  const onMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    const el = e.currentTarget;\n    const rect = el.getBoundingClientRect();\n    if (!rect.width) return;\n    const k = el.offsetWidth / rect.width;\n    el.style.setProperty(\"--gbb-x\", ((e.clientX - rect.left) * k).toFixed(1) + \"px\");\n    el.style.setProperty(\"--gbb-y\", ((e.clientY - rect.top) * k).toFixed(1) + \"px\");\n  };\n\n  // Tail stacked from shorter and shorter dashes that share one head, so the\n  // light fades behind the tip. [fraction of length, colour, opacity]\n  const full: [number, string, number][] = [\n    ...Array.from({ length: 16 }, (_, i): [number, string, number] => [Math.pow((16 - i) / 16, 1.7), tail, 0.075]),\n    [0.2, core, 0.2],\n    [0.14, core, 0.26],\n    [0.09, core, 0.32],\n    [0.05, core, 0.9],\n  ];\n  const soft: [number, string, number][] = [\n    [1, tail, 0.3],\n    [0.55, tail, 0.45],\n    [0.2, core, 0.5],\n  ];\n\n  const beam = (segs: [number, string, number][], rev: boolean, delay: number, cls: string) =>\n    segs.map(([f, c, o], i) => {\n      const l = len * f;\n      // Every dash of a beam shares one head: the forward head is the dash end, the reverse head its start.\n      const lead = rev ? 30 : len - l;\n      const dash = \"0 \" + lead.toFixed(2) + \" \" + l.toFixed(2) + \" \" + (100 - lead - l).toFixed(2);\n      return (\n        <rect\n          key={cls + i}\n          className={\"gbb-seg \" + cls + (rev ? \" gbb-rev\" : \" gbb-fwd\")}\n          width=\"100%\"\n          height=\"100%\"\n          rx={Math.max(0, r - bw / 2)}\n          fill=\"none\"\n          stroke={c}\n          strokeOpacity={o}\n          pathLength={100}\n          strokeDasharray={dash}\n          style={{ animationDelay: delay + \"s\" }}\n        />\n      );\n    });\n\n  const ring = (segs: [number, string, number][], cls: string) => (\n    <>\n      {beam(segs, reverse, 0, cls)}\n      {dual ? beam(segs, !reverse, -lap / 2, cls) : null}\n    </>\n  );\n  const svgBase = (extra: CSSProperties): CSSProperties => ({\n    position: \"absolute\",\n    left: bw / 2,\n    top: bw / 2,\n    width: \"calc(100% - \" + bw + \"px)\",\n    height: \"calc(100% - \" + bw + \"px)\",\n    overflow: \"visible\",\n    pointerEvents: \"none\",\n    ...extra,\n  });\n\n  return (\n    <div\n      ref={rootRef}\n      data-live=\"1\"\n      className={\"gbb-root relative isolate \" + className}\n      style={\n        {\n          borderRadius: r,\n          \"--gbb-lap\": lap + \"s\",\n          \"--gbb-bw\": bw + \"px\",\n          \"--gbb-x\": \"50%\",\n          \"--gbb-y\": \"50%\",\n        } as CSSProperties\n      }\n    >\n      {/* Light that escapes past the edge, behind the card */}\n      {g > 0 ? (\n        <svg aria-hidden className=\"gbb-out\" style={svgBase({ opacity: 0.55 * g, filter: \"blur(10px)\", zIndex: -1 })} fill=\"none\">\n          <g strokeWidth={bw * 3}>{ring(soft, \"gbb-g\")}</g>\n        </svg>\n      ) : null}\n      <div\n        onPointerMove={onMove}\n        className={\"relative overflow-hidden \" + (hasChildren ? \"\" : \"flex h-32 w-64 items-center justify-center\")}\n        style={{ borderRadius: r, background: bg, color: \"#fff\" }}\n      >\n        {/* Light that bleeds onto the surface */}\n        {g > 0 ? (\n          <svg aria-hidden style={svgBase({ left: bw / 2, top: bw / 2, opacity: 0.4 * g, filter: \"blur(14px)\" })} fill=\"none\">\n            <g strokeWidth={bw * 9}>{ring(soft, \"gbb-g\")}</g>\n          </svg>\n        ) : null}\n        {/* Pointer light */}\n        <div\n          aria-hidden\n          className=\"gbb-spot pointer-events-none absolute inset-0\"\n          style={{ background: \"radial-gradient(260px circle at var(--gbb-x) var(--gbb-y), rgba(255,255,255,0.075), transparent 70%)\" }}\n        />\n        <div className={\"relative z-10 \" + (hasChildren ? \"\" : \"text-sm font-semibold tracking-tight\")}>{hasChildren ? children : label}</div>\n      </div>\n      {/* The beam itself, drawn over the edge */}\n      <svg aria-hidden style={svgBase({ zIndex: 2 })} fill=\"none\">\n        <rect\n          width=\"100%\"\n          height=\"100%\"\n          rx={Math.max(0, r - bw / 2)}\n          stroke=\"rgba(255,255,255,0.1)\"\n          strokeWidth={bw}\n        />\n        <g>\n          {ring(full, \"gbb-b\")}\n        </g>\n      </svg>\n      <style>{`\n        .gbb-seg { stroke-dashoffset: -8; transition: stroke-width 300ms ease; animation-duration: var(--gbb-lap, 4s); animation-timing-function: linear; animation-iteration-count: infinite; }\n        .gbb-seg.gbb-fwd { animation-name: gbb-fwd; }\n        .gbb-seg.gbb-rev { animation-name: gbb-rev; stroke-dashoffset: 8; }\n        .gbb-root[data-live=\"0\"] .gbb-seg { animation-play-state: paused; }\n        .gbb-b { stroke-width: var(--gbb-bw); }\n        .gbb-root:hover .gbb-b, .gbb-root:focus-within .gbb-b { stroke-width: calc(var(--gbb-bw) * 1.6); }\n        .gbb-out, .gbb-spot { transition: opacity 400ms ease; }\n        .gbb-spot { opacity: 0; }\n        .gbb-root:hover .gbb-spot { opacity: 1; }\n        .gbb-root:hover .gbb-out, .gbb-root:focus-within .gbb-out { opacity: 0.9 !important; }\n        @keyframes gbb-fwd { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } }\n        @keyframes gbb-rev { from { stroke-dashoffset: 0; } to { stroke-dashoffset: 100; } }\n        @media (prefers-reduced-motion: reduce) { .gbb-seg { animation: none !important; } }\n      `}</style>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/viberdy/gradient-border-beam.tsx"}],"meta":{"tier":"free","tags":["border","beam","comet","card","glow","svg","wrapper"],"href":"https://viberdy.dev/library/gradient-border-beam","aiPrompt":"Create a single-file React + Tailwind component named BorderBeamCard (\"use client\", React only, no animation library). It wraps children in a rounded card with a comet of light travelling around the border.\n\nProps (all optional): label \"Border beam\" (shown centred in a h-32 w-64 box when there are no children); speed 4 (seconds per lap); children; color \"#ff2d2d\" (tail); coreColor \"#ffffff\" (head); size 22 (beam length, percent of the perimeter, clamp 4-60); borderWidth 2 (px, clamp 1-8); radius 16 (px); glow 1 (0-2); dual false (second beam in the opposite direction, half a lap out of phase); reverse false (counter-clockwise); surface \"#0b0c0f\"; className (on the root).\n\nStructure: root = relative isolate div with border-radius, CSS vars --gbb-lap (speed s) and --gbb-bw. Inside: (1) an outer-glow svg behind the card (z -1), absolutely positioned inset borderWidth/2, overflow visible, blur(10px), opacity 0.55*glow, strokes 3x border width; (2) the surface div, overflow hidden, border-radius, background surface, white text, containing a second blurred svg (blur 14px, opacity 0.4*glow, stroke 9x border width) for the inner glow, a pointer-light div (radial-gradient 260px circle at --gbb-x/--gbb-y, white 7.5%, fading by 70%, opacity 0 until the root is hovered, 400ms), and the content at z-10; (3) the beam svg on top (z 2) with a static hairline rect (rgba(255,255,255,0.1)) plus the beam.\n\nThe beam is an svg rect, width/height 100%, rx = radius - borderWidth/2, fill none, pathLength=100 so lengths are percentages and the speed is even around corners. Draw it as twenty stroke-dasharray dashes sharing one head: sixteen in color with lengths size * ((16-i)/16)^1.7 (i = 0..15) at stroke opacity .075 each, then four in coreColor with lengths size * [.2, .14, .09, .05] at opacity [.2, .26, .32, .9]. The glow layers use three dashes (1, .55, .2 at .3/.45/.5). Dasharray is \"0 lead len rest\" with lead = (size - len) forward (so every dash ends at the same head) or 30 reverse (every dash starts at the head). Forward animation: stroke-dashoffset 0 to -100, linear, infinite, duration speed; reverse animation: 0 to 100. Dual draws a second set in the other direction with animation-delay -speed/2.\n\nInteraction: pointermove on the surface writes --gbb-x/--gbb-y in layout px ((clientX - rect.left) * offsetWidth / rect.width), no React state. Hover or focus-within: beam stroke-width x1.6 (300ms ease), outer glow opacity 0.9. An IntersectionObserver (rootMargin 120px) sets data-live on the root and the CSS pauses the animation when \"0\". Validate colour props with a regex before using them.\n\nprefers-reduced-motion: animation none; the beam rests as a still comet (dashoffset -8 forward, 8 reverse). The decoration is aria-hidden and pointer-events none, so the card is as accessible as its children. Output the single file.","license":"https://viberdy.dev/license"}}