{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "border-beam",
  "type": "registry:component",
  "title": "Border Beam",
  "description": "A beam of light with a tail, a bright head and a halo that travels around the outline of any content at constant speed.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Cards"
  ],
  "docs": "Installed in components/furin/border-beam/. Docs, live controls and props table: https://furin.dev/components/border-beam/",
  "files": [
    {
      "path": "registry/border-beam/BorderBeam.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport \"./BorderBeam.css\";\n\nexport interface BorderBeamProps {\n  /** Contenido de la tarjeta; si es texto se muestra como título. */\n  children?: ReactNode;\n  /** Texto de apoyo bajo el título (solo cuando children es texto). */\n  caption?: string;\n  /** Color de la cabeza del haz. */\n  color?: string;\n  /** Color de la cola del haz. */\n  colorTo?: string;\n  /** Segundos que tarda el haz en dar una vuelta. */\n  duration?: number;\n  /** Largo de la cola, en px. */\n  size?: number;\n  /** Grosor del borde iluminado, en px. */\n  borderWidth?: number;\n  /** Radio de las esquinas, en px. */\n  radius?: number;\n  /** Cuántos haces recorren el borde a la vez. */\n  beams?: number;\n  /** Intensidad del halo, de 0 a 1.5. */\n  glow?: number;\n  /** Ancho máximo de la tarjeta, en px. */\n  width?: number;\n  /** Alto mínimo de la tarjeta, en px. */\n  minHeight?: number;\n  /** Fondo de la tarjeta. */\n  background?: string;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\nconst PAD = 28; // margen del canvas para el halo exterior\n\n/** Lee cualquier color CSS como [r, g, b]. */\nfunction parseColor(c: string): [number, number, number] {\n  const cv = document.createElement(\"canvas\");\n  cv.width = cv.height = 1;\n  const x = cv.getContext(\"2d\", { willReadFrequently: true });\n  if (!x) return [255, 255, 255];\n  x.fillStyle = \"#fff\";\n  x.fillStyle = c;\n  x.fillRect(0, 0, 1, 1);\n  const d = x.getImageData(0, 0, 1, 1).data;\n  return [d[0], d[1], d[2]];\n}\nconst mix = (a: number[], b: number[], t: number) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n\n/** Puntos equiespaciados (por longitud de arco) sobre un rectángulo redondeado: la velocidad del haz es constante hasta en las esquinas. */\nfunction buildPath(w: number, h: number, r: number, ds: number) {\n  const sw = Math.max(0, w - 2 * r), sh = Math.max(0, h - 2 * r), arc = (Math.PI / 2) * r;\n  const P = 2 * sw + 2 * sh + 4 * arc;\n  const n = Math.max(16, Math.round(P / ds));\n  const xs = new Float32Array(n), ys = new Float32Array(n);\n  for (let i = 0; i < n; i++) {\n    let d = (i / n) * P, x = 0, y = 0, a = 0;\n    if (d < sw) { x = r + d; y = 0; }\n    else if ((d -= sw) < arc) { a = -Math.PI / 2 + d / r; x = w - r + r * Math.cos(a); y = r + r * Math.sin(a); }\n    else if ((d -= arc) < sh) { x = w; y = r + d; }\n    else if ((d -= sh) < arc) { a = d / r; x = w - r + r * Math.cos(a); y = h - r + r * Math.sin(a); }\n    else if ((d -= arc) < sw) { x = w - r - d; y = h; }\n    else if ((d -= sw) < arc) { a = Math.PI / 2 + d / r; x = r + r * Math.cos(a); y = h - r + r * Math.sin(a); }\n    else if ((d -= arc) < sh) { x = 0; y = h - r - d; }\n    else { d -= sh; a = Math.PI + d / r; x = r + r * Math.cos(a); y = r + r * Math.sin(a); }\n    xs[i] = x; ys[i] = y;\n  }\n  return { xs, ys, n, P };\n}\n\n/** Haz de luz que recorre el contorno de cualquier contenido, con cola degradada, cabeza brillante y halo. */\nexport function BorderBeam({\n  children = \"A beam traces the border\",\n  caption = \"Over any content, with a tail and a glow.\",\n  color = \"#8fb4ff\",\n  colorTo = \"#b46cff\",\n  duration = 6,\n  size = 220,\n  borderWidth = 1.6,\n  radius = 22,\n  beams = 1,\n  glow = 1,\n  width = 360,\n  minHeight = 0,\n  background = \"#0f1116\",\n  className = \"\",\n  style,\n}: BorderBeamProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const cvRef = useRef<HTMLCanvasElement>(null);\n  const cfg = useRef({ color, colorTo, duration, size, borderWidth, radius, beams, glow });\n  cfg.current = { color, colorTo, duration, size, borderWidth, radius, beams, glow };\n  const redraw = useRef<() => void>(() => {});\n\n  useEffect(() => {\n    const root = rootRef.current, cv = cvRef.current;\n    if (!root || !cv) return;\n    const ctx = cv.getContext(\"2d\");\n    if (!ctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let W = 0, H = 0, dpr = 1, raf = 0, visible = true, t0 = 0;\n    let path = buildPath(10, 10, 4, 2);\n    let ramp: HTMLCanvasElement[] = [];\n    let rgbFrom: number[] = [255, 255, 255], rgbTo: number[] = [255, 255, 255];\n    let key = \"\";\n\n    /** Sprites de brillo: un punto difuso por cada paso del degradado de color, para estampar el halo sin filtros. */\n    const makeSprites = () => {\n      const c = cfg.current;\n      rgbFrom = parseColor(c.color); rgbTo = parseColor(c.colorTo);\n      ramp = Array.from({ length: 8 }, (_, i) => {\n        const s = document.createElement(\"canvas\");\n        s.width = s.height = 64;\n        const g = s.getContext(\"2d\")!;\n        const col = mix(rgbTo, rgbFrom, i / 7).map(Math.round);\n        const gr = g.createRadialGradient(32, 32, 0, 32, 32, 32);\n        gr.addColorStop(0, `rgba(${col},1)`);\n        gr.addColorStop(0.25, `rgba(${col},0.55)`);\n        gr.addColorStop(0.6, `rgba(${col},0.13)`);\n        gr.addColorStop(1, `rgba(${col},0)`);\n        g.fillStyle = gr;\n        g.fillRect(0, 0, 64, 64);\n        return s;\n      });\n    };\n\n    const layout = () => {\n      const c = cfg.current;\n      const w = root.offsetWidth, h = root.offsetHeight;\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      W = w + PAD * 2; H = h + PAD * 2;\n      cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);\n      const r = Math.min(c.radius, w / 2, h / 2);\n      const inset = c.borderWidth / 2;\n      path = buildPath(w - c.borderWidth, h - c.borderWidth, Math.max(0, r - inset), 2);\n      const k = `${c.color}|${c.colorTo}`;\n      if (k !== key) { key = k; makeSprites(); }\n    };\n\n    const draw = (t: number) => {\n      const c = cfg.current;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, W, H);\n      ctx.globalCompositeOperation = \"lighter\";\n      // Cabos planos: con cabos redondos cada tramo se solapa con el siguiente y se forman cuentas.\n      ctx.lineCap = \"butt\";\n      ctx.lineJoin = \"round\";\n      const { xs, ys, n, P } = path;\n      const ds = P / n;\n      const m = Math.min(n - 2, Math.max(8, c.size / ds));\n      const off = PAD + c.borderWidth / 2;\n      const bw = c.borderWidth;\n      for (let b = 0; b < Math.max(1, Math.round(c.beams)); b++) {\n        const head = ((t / (c.duration * 1000) + b / Math.max(1, Math.round(c.beams))) % 1) * n;\n        const at = (k: number) => { const i = (((Math.floor(head - k) % n) + n) % n); return i; };\n        // Halo: puntos difusos que se encogen hacia la cola.\n        for (let k = 0; k <= m; k += 5) {\n          const f = 1 - k / m;\n          const i = at(k);\n          const rad = bw * (4 + 11 * f) * (0.6 + 0.4 * c.glow);\n          ctx.globalAlpha = Math.min(1, f * f * 0.42 * c.glow);\n          const sprite = ramp[Math.round(Math.min(1, f * 1.15) * 7)];\n          ctx.drawImage(sprite, off + xs[i] - rad, off + ys[i] - rad, rad * 2, rad * 2);\n        }\n        // Núcleo: trazo cortito con ancho y opacidad que crecen hacia la cabeza.\n        const seg = 3;\n        for (let k = m; k > 0; k -= seg) {\n          const f = 1 - k / m;\n          const f2 = 1 - (k - seg) / m;\n          const col = mix(rgbTo, rgbFrom, Math.min(1, f * f * 1.1));\n          const wh = Math.max(0, (f - 0.9) / 0.1);\n          const rgb = mix(col, [255, 255, 255], wh * 0.85);\n          ctx.strokeStyle = `rgb(${rgb[0] | 0},${rgb[1] | 0},${rgb[2] | 0})`;\n          ctx.globalAlpha = Math.min(1, Math.pow(f2, 1.15) * 1.1);\n          ctx.lineWidth = bw * (0.55 + 0.75 * f2);\n          ctx.beginPath();\n          for (let q = 0; q <= seg; q++) {\n            const i = at(k - q);\n            if (q === 0) ctx.moveTo(off + xs[i], off + ys[i]); else ctx.lineTo(off + xs[i], off + ys[i]);\n          }\n          ctx.stroke();\n        }\n        // Cabeza\n        const hi = at(0);\n        const hs = ramp[7];\n        ctx.globalAlpha = 0.9;\n        ctx.drawImage(hs, off + xs[hi] - bw * 5, off + ys[hi] - bw * 5, bw * 10, bw * 10);\n        ctx.globalAlpha = 1;\n        ctx.fillStyle = \"#fff\";\n        ctx.beginPath();\n        ctx.arc(off + xs[hi], off + ys[hi], bw * 0.7, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const frame = (now: number) => {\n      raf = 0;\n      if (!t0) t0 = now;\n      draw(now - t0);\n      if (visible) raf = requestAnimationFrame(frame);\n    };\n    const start = () => { if (!raf && !reduced) raf = requestAnimationFrame(frame); };\n\n    layout();\n    if (reduced) draw(cfg.current.duration * 1000 * 0.1); else start();\n    redraw.current = () => { layout(); if (reduced) draw(cfg.current.duration * 1000 * 0.1); };\n\n    const ro = new ResizeObserver(() => { layout(); if (reduced) draw(cfg.current.duration * 1000 * 0.1); });\n    ro.observe(root);\n    const io = new IntersectionObserver((e) => {\n      visible = e[0]?.isIntersecting ?? true;\n      if (visible) start(); else { cancelAnimationFrame(raf); raf = 0; }\n    });\n    io.observe(root);\n    return () => { cancelAnimationFrame(raf); ro.disconnect(); io.disconnect(); };\n  }, []);\n\n  // Si cambian las props visuales con el componente montado, se recalcula la geometría y los colores.\n  useEffect(() => { redraw.current(); }, [color, colorTo, borderWidth, radius, size, beams]);\n\n  const vars = {\n    \"--furin-border-beam-w\": `${width}px`,\n    \"--furin-border-beam-r\": `${radius}px`,\n    \"--furin-border-beam-bg\": background,\n    \"--furin-border-beam-min\": `${minHeight}px`,\n    \"--furin-border-beam-color\": color,\n    \"--furin-border-beam-pad\": `${PAD}px`,\n  } as CSSProperties;\n\n  return (\n    <div ref={rootRef} className={`furin-border-beam ${className}`} style={{ ...vars, ...style }}>\n      <div className=\"furin-border-beam__content\">\n        {typeof children === \"string\" ? (\n          <>\n            <h3 className=\"furin-border-beam__title\">{children}</h3>\n            {caption ? <p className=\"furin-border-beam__caption\">{caption}</p> : null}\n          </>\n        ) : children}\n      </div>\n      <canvas ref={cvRef} className=\"furin-border-beam__canvas\" aria-hidden=\"true\" />\n    </div>\n  );\n}\n\nexport default BorderBeam;\n",
      "type": "registry:component",
      "target": "components/furin/border-beam/BorderBeam.tsx"
    },
    {
      "path": "registry/border-beam/BorderBeam.css",
      "content": ".furin-border-beam {\n  position: relative;\n  box-sizing: border-box;\n  width: min(var(--furin-border-beam-w), calc(100% - 48px));\n  min-height: var(--furin-border-beam-min);\n  display: grid;\n  border-radius: var(--furin-border-beam-r);\n  background: var(--furin-border-beam-bg);\n  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 30px 60px -30px rgb(0 0 0 / 0.8);\n  color: #f2f3f7;\n  font-family: Geist, -apple-system, BlinkMacSystemFont, \"SF Pro Text\", system-ui, sans-serif;\n}\n.furin-border-beam__content {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  padding: 32px 32px;\n  border-radius: inherit;\n  /* Un velo de luz arriba, como una tarjeta de vidrio oscuro. */\n  background: radial-gradient(120% 90% at 50% -20%, rgb(255 255 255 / 0.06), transparent 60%);\n}\n.furin-border-beam__title { margin: 0; font: 650 26px/1.15 Geist, system-ui, sans-serif; letter-spacing: -0.025em; }\n.furin-border-beam__caption { margin: 10px 0 0; font: 400 15px/1.5 Geist, system-ui, sans-serif; color: rgb(255 255 255 / 0.55); max-width: 30ch; }\n.furin-border-beam__canvas {\n  position: absolute;\n  left: calc(var(--furin-border-beam-pad) * -1);\n  top: calc(var(--furin-border-beam-pad) * -1);\n  width: calc(100% + var(--furin-border-beam-pad) * 2);\n  height: calc(100% + var(--furin-border-beam-pad) * 2);\n  pointer-events: none;\n}\n",
      "type": "registry:file",
      "target": "components/furin/border-beam/BorderBeam.css"
    }
  ]
}
