{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"border-beam","type":"registry:ui","title":"BorderBeam","description":"A single comet of light riding a container's border: a white-hot head, a long feathered tail fading between two colours, a dim hairline that lights up only within the comet's reach, and a soft bloom that sits on the edge.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["effects"],"registryDependencies":["https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/border-beam/BorderBeam.tsx","content":"\"use client\";\n\nimport type { CSSProperties } from \"react\";\nimport { cn } from \"../../utils\";\nimport \"./border-beam.css\";\n\n/**\n * BorderBeam - a single comet of light riding a container's border\n *\n * A white-hot head leads a long, feathered tail that fades from `colorFrom`\n * to `colorTo` to nothing. The hairline under it stays dim except within\n * the comet's reach, a soft bloom sits on the edge, and a faint spill of\n * light falls just inside the container.\n *\n * Place it as the last child of a container with `position: relative`\n * (and usually `overflow: hidden`). It inherits the container's\n * `border-radius`; set `--border-beam-radius` if the path needs a radius\n * the browser cannot read from the box itself.\n */\nexport interface BorderBeamProps {\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/** Diameter of the pool of light around the comet's head, in pixels */\n\tsize?: number;\n\t/** Time for one full lap of the border, in seconds */\n\tduration?: number;\n\t/** Border width in pixels */\n\tborderWidth?: number;\n\t/**\n\t * Where along the border the comet starts, as a percentage of the\n\t * perimeter (0-100, clockwise from the top-left corner). With reduced\n\t * motion, this is where it rests.\n\t */\n\tanchor?: number;\n\t/** Colour of the comet's head and the start of its tail */\n\tcolorFrom?: string;\n\t/** Colour the tail fades into */\n\tcolorTo?: string;\n\t/**\n\t * Delay in seconds. The comet runs from the first frame, already\n\t * `delay` seconds behind where it would otherwise be, so stacked beams\n\t * spread out along the border.\n\t */\n\tdelay?: number;\n\t/** Length of the tail as a share of the perimeter (0-1) */\n\ttail?: number;\n\t/** Strength of the bloom and the inner spill of light (0-1) */\n\tglow?: number;\n\t/** Travel counter-clockwise instead of clockwise */\n\treverse?: boolean;\n}\n\n/** Number of overlapping pieces the sharp tail is drawn with. More pieces\n * bend around the corners more smoothly. */\nexport const TAIL_SEGMENTS = 18;\n/** The blurred bloom is forgiving: fewer, longer pieces are enough. */\nexport const BLOOM_SEGMENTS = 8;\n\nexport interface TailSegment {\n\t/** Position along the tail, 0 = right behind the head, 1 = tail end */\n\tt: number;\n\t/** Opacity of this piece */\n\talpha: number;\n\t/** Thickness factor (1 at the head, tapering towards the end) */\n\tthickness: number;\n}\n\n/** Evenly spaced pieces with an eased fade and a tapering thickness. */\nexport function tailSegments(count: number): TailSegment[] {\n\treturn Array.from({ length: count }, (_, i) => {\n\t\tconst t = (i + 0.5) / count;\n\t\treturn {\n\t\t\tt: Number(t.toFixed(4)),\n\t\t\talpha: Number(Math.pow(1 - t, 1.6).toFixed(4)),\n\t\t\tthickness: Number((1 - 0.7 * t).toFixed(4)),\n\t\t};\n\t});\n}\n\nfunction clamp01(n: number): number {\n\treturn Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;\n}\n\n/**\n * Turn `delay` into a phase: how far into its lap the comet is at first\n * paint (a negative `animation-delay`), and where it rests when motion is\n * reduced (a percentage of the perimeter).\n */\nexport function beamPhase(\n\tduration: number,\n\tdelay: number,\n\tanchor: number,\n\treverse: boolean\n): { offset: number; park: number } {\n\tconst d = duration > 0 ? duration : 1;\n\tconst lag = ((delay % d) + d) % d;\n\tconst offset = lag === 0 ? 0 : -(d - lag);\n\tconst dir = reverse ? -1 : 1;\n\tconst park = anchor - (dir * lag * 100) / d;\n\treturn { offset: Number(offset.toFixed(4)), park: Number(park.toFixed(4)) };\n}\n\n// The piece lists never change: computed once at module load, like the\n// Svelte component's per-instance constants.\nconst tailPieces = tailSegments(TAIL_SEGMENTS);\nconst bloomPieces = tailSegments(BLOOM_SEGMENTS);\n\nfunction piece(p: TailSegment, count: number): CSSProperties {\n\treturn {\n\t\t\"--bb-t\": p.t,\n\t\t\"--bb-alpha\": p.alpha,\n\t\t\"--bb-thick\": p.thickness,\n\t\t\"--bb-count\": count,\n\t} as CSSProperties;\n}\n\nexport function BorderBeam({\n\tclassName,\n\tsize = 200,\n\tduration = 9,\n\tborderWidth = 1.5,\n\tanchor = 90,\n\tcolorFrom = \"#8ec5ff\",\n\tcolorTo = \"#c084fc\",\n\tdelay = 0,\n\ttail = 0.25,\n\tglow = 0.6,\n\treverse = false,\n}: BorderBeamProps) {\n\tconst phase = beamPhase(duration, delay, anchor, reverse);\n\n\tconst style = {\n\t\t\"--border-beam-size\": size,\n\t\t\"--border-beam-duration\": `${duration}s`,\n\t\t\"--border-beam-anchor\": anchor,\n\t\t\"--border-beam-border-width\": borderWidth,\n\t\t\"--border-beam-color-from\": colorFrom,\n\t\t\"--border-beam-color-to\": colorTo,\n\t\t\"--border-beam-delay\": `${delay}s`,\n\t\t\"--border-beam-tail\": clamp01(tail),\n\t\t\"--border-beam-glow\": clamp01(glow),\n\t\t\"--border-beam-offset\": `${phase.offset}s`,\n\t\t\"--border-beam-park\": `${phase.park}%`,\n\t\t\"--border-beam-dir\": reverse ? -1 : 1,\n\t} as CSSProperties;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"border-beam\",\n\t\t\t\t\"pointer-events-none absolute inset-0 rounded-[inherit]\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tdata-direction={reverse ? \"reverse\" : \"forward\"}\n\t\t\taria-hidden=\"true\"\n\t\t\tstyle={style}\n\t\t>\n\t\t\t{/* faint light falling just inside the edge, clipped to the container */}\n\t\t\t<div className=\"bb-spill-clip\">\n\t\t\t\t<span className=\"bb-ride bb-spill\" />\n\t\t\t</div>\n\n\t\t\t{/* the bloom: a blurred copy of the comet straddling the edge */}\n\t\t\t<div className=\"bb-bloom\">\n\t\t\t\t{bloomPieces.map((p) => (\n\t\t\t\t\t<span\n\t\t\t\t\t\tkey={p.t}\n\t\t\t\t\t\tclassName=\"bb-ride bb-piece bb-bloom-piece\"\n\t\t\t\t\t\tstyle={piece(p, BLOOM_SEGMENTS)}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t\t<span className=\"bb-ride bb-bloom-head\" />\n\t\t\t</div>\n\n\t\t\t{/* the halo: a tight glow hugging the lit hairline */}\n\t\t\t<div className=\"bb-halo\">\n\t\t\t\t{bloomPieces.map((p) => (\n\t\t\t\t\t<span\n\t\t\t\t\t\tkey={p.t}\n\t\t\t\t\t\tclassName=\"bb-ride bb-piece bb-halo-piece\"\n\t\t\t\t\t\tstyle={piece(p, BLOOM_SEGMENTS)}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</div>\n\n\t\t\t{/* the hairline: dim, lit only within the comet's reach */}\n\t\t\t<div className=\"bb-ring\">\n\t\t\t\t<span className=\"bb-rail\" />\n\t\t\t\t<span className=\"bb-ride bb-reach\" />\n\t\t\t\t{tailPieces.map((p) => (\n\t\t\t\t\t<span\n\t\t\t\t\t\tkey={p.t}\n\t\t\t\t\t\tclassName=\"bb-ride bb-piece bb-tail-piece\"\n\t\t\t\t\t\tstyle={piece(p, TAIL_SEGMENTS)}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t\t<span className=\"bb-ride bb-head\" />\n\t\t\t</div>\n\n\t\t\t{/* the white-hot point of the head, sitting on the edge */}\n\t\t\t<span className=\"bb-ride bb-spark\" />\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/border-beam/BorderBeam.tsx"},{"path":"react/src/components/border-beam/border-beam.css","content":"/*\n * Port note: the Svelte source's rules are compiler-scoped. Here every rule is\n * anchored on the component's own root class `.border-beam` so the `bb-*`\n * hooks never match outside the component; the dark-theme block reads\n * `.dark .border-beam` (the Svelte `:global(.dark)` selector).\n */\n\n.border-beam {\n\t/* private plumbing: every value reads a public var with a fallback */\n\t--_from: var(--border-beam-color-from, #8ec5ff);\n\t--_to: var(--border-beam-color-to, #c084fc);\n\t--_bw: calc(var(--border-beam-border-width, 1.5) * 1px);\n\t--_size: calc(var(--border-beam-size, 200) * 1px);\n\t--_dur: var(--border-beam-duration, 9s);\n\t--_tail: var(--border-beam-tail, 0.25);\n\t--_glow: var(--border-beam-glow, 0.6);\n\t--_offset: var(--border-beam-offset, 0s);\n\t--_start: calc(var(--border-beam-anchor, 90) * 1%);\n\t--_park: var(--border-beam-park, 90%);\n\t--_dir: var(--border-beam-dir, 1);\n\t--_radius: var(--border-beam-radius, 12px);\n\t/* perimeter of the box, read through container units */\n\t--_perimeter: calc(2 * (100cqw + 100cqh));\n\n\t/* light theme: inks a touch deeper, head warm rather than white, quieter bloom */\n\t--_ink-from: color-mix(in oklab, var(--_from) 76%, #0b0b0c);\n\t--_ink-to: color-mix(in oklab, var(--_to) 76%, #0b0b0c);\n\t--_hot: color-mix(in oklab, var(--_from) 80%, white);\n\t--_core: color-mix(in oklab, var(--_ink-from) 70%, white);\n\t--_rail: color-mix(in oklab, var(--_ink-from) 6%, transparent);\n\t--_bloom-max: 0.7;\n\t--_halo-max: 0.7;\n\t--_spill-max: 0.16;\n\n\tcontainer-type: size;\n\tisolation: isolate;\n}\n\n.dark .border-beam {\n\t--_ink-from: var(--_from);\n\t--_ink-to: var(--_to);\n\t--_hot: color-mix(in oklab, var(--_from) 40%, white);\n\t--_core: white;\n\t--_rail: color-mix(in oklab, var(--_from) 7%, transparent);\n\t--_bloom-max: 1;\n\t--_halo-max: 0.85;\n\t--_spill-max: 0.24;\n}\n\n.border-beam > *,\n.border-beam .bb-ring > *,\n.border-beam .bb-bloom > *,\n.border-beam .bb-halo > *,\n.border-beam .bb-spill-clip > * {\n\tposition: absolute;\n\tpointer-events: none;\n}\n\n.border-beam .bb-spill-clip,\n.border-beam .bb-bloom,\n.border-beam .bb-halo,\n.border-beam .bb-ring,\n.border-beam .bb-rail {\n\tinset: 0;\n\tborder-radius: inherit;\n}\n\n/* ---------- riders: everything that travels the border ---------- */\n\n.border-beam .bb-ride {\n\ttop: 0;\n\tleft: 0;\n\t/* fallback path first, then the box itself with its real radius */\n\toffset-path: rect(0 auto auto 0 round var(--_radius));\n\toffset-path: border-box;\n\toffset-rotate: auto;\n\toffset-anchor: 50% 50%;\n\t/* rest pose (reduced motion): parked at the anchor, tail laid out behind */\n\toffset-distance: calc(var(--_park) - var(--_dir) * var(--_lag, 0) * 100%);\n\t--_lag: 0;\n}\n\n.border-beam[data-direction=\"reverse\"] .bb-ride {\n\toffset-rotate: reverse;\n}\n\n.border-beam .bb-piece {\n\t--_lag: calc(var(--_tail) * var(--bb-t));\n\t/* pieces overlap twice over so the tail reads as one stroke */\n\twidth: calc(var(--_perimeter) * var(--_tail) / var(--bb-count) * 2.2);\n}\n\n/* ---------- the sharp comet on the hairline ---------- */\n\n.border-beam .bb-ring {\n\tpadding: var(--_bw);\n\t-webkit-mask:\n\t\tlinear-gradient(#000 0 0) content-box,\n\t\tlinear-gradient(#000 0 0);\n\t-webkit-mask-composite: xor;\n\tmask:\n\t\tlinear-gradient(#000 0 0) content-box,\n\t\tlinear-gradient(#000 0 0);\n\tmask-composite: exclude;\n}\n\n.border-beam .bb-rail {\n\tposition: absolute;\n\tbackground: var(--_rail);\n}\n\n.border-beam .bb-reach {\n\twidth: var(--_size);\n\theight: var(--_size);\n\tborder-radius: 50%;\n\tbackground: radial-gradient(\n\t\tclosest-side,\n\t\tcolor-mix(in oklab, var(--_hot) 70%, transparent),\n\t\tcolor-mix(in oklab, var(--_ink-from) 40%, transparent) 45%,\n\t\ttransparent\n\t);\n\topacity: 0.7;\n}\n\n.border-beam .bb-tail-piece {\n\theight: calc(var(--_bw) * 2 + 4px);\n\topacity: var(--bb-alpha);\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tcolor-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to)) 30%,\n\t\tcolor-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to)) 70%,\n\t\ttransparent\n\t);\n}\n\n.border-beam .bb-head {\n\twidth: 44px;\n\theight: calc(var(--_bw) * 2 + 4px);\n\toffset-anchor: 100% 50%;\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tvar(--_ink-from) 45%,\n\t\tvar(--_hot) 85%,\n\t\tvar(--_core)\n\t);\n}\n\n/* ---------- bloom: the comet again, thicker and blurred ---------- */\n\n.border-beam .bb-bloom {\n\tfilter: blur(11px);\n\topacity: calc(var(--_glow) * var(--_bloom-max));\n}\n\n.border-beam .bb-bloom-piece {\n\theight: calc(var(--_bw) + 16px * var(--bb-thick));\n\topacity: var(--bb-alpha);\n\tborder-radius: 999px;\n\tbackground: color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to));\n}\n\n.border-beam .bb-bloom-head {\n\twidth: 56px;\n\theight: 22px;\n\toffset-anchor: 85% 50%;\n\tborder-radius: 50%;\n\tbackground: radial-gradient(closest-side, var(--_hot), var(--_ink-from) 60%, transparent);\n}\n\n/* ---------- halo: a thin, lightly blurred glow right on the line ---------- */\n\n.border-beam .bb-halo {\n\tfilter: blur(2.5px);\n\topacity: calc(var(--_glow) * var(--_halo-max));\n}\n\n.border-beam .bb-halo-piece {\n\theight: calc(var(--_bw) + 5px * var(--bb-thick));\n\topacity: var(--bb-alpha);\n\tborder-radius: 999px;\n\tbackground: color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to));\n}\n\n/* ---------- inner spill: a wide, faint pool clipped to the inside ---------- */\n\n.border-beam .bb-spill-clip {\n\toverflow: hidden;\n}\n\n.border-beam .bb-spill {\n\twidth: calc(var(--_size) * 1.3);\n\theight: calc(var(--_size) * 1.3);\n\tborder-radius: 50%;\n\tbackground: radial-gradient(\n\t\tclosest-side,\n\t\tcolor-mix(in oklab, var(--_ink-from) 70%, transparent),\n\t\tcolor-mix(in oklab, var(--_ink-to) 25%, transparent) 50%,\n\t\ttransparent\n\t);\n\topacity: calc(var(--_glow) * var(--_spill-max));\n}\n\n/* ---------- the spark: a tiny white-hot point on the edge ---------- */\n\n.border-beam .bb-spark {\n\twidth: calc(var(--_bw) * 2 + 8px);\n\theight: calc(var(--_bw) * 2 + 8px);\n\tborder-radius: 50%;\n\tbackground: radial-gradient(\n\t\tclosest-side,\n\t\tvar(--_core),\n\t\tvar(--_hot) 30%,\n\t\tcolor-mix(in oklab, var(--_hot) 35%, transparent) 62%,\n\t\ttransparent\n\t);\n}\n\n/* ---------- motion ---------- */\n\n@media (prefers-reduced-motion: no-preference) {\n\t.border-beam {\n\t\t/* arrival: the comet fades in over the entrance duration, up to the root's own\n\t\t   opacity (no `to` keyframe, no forwards fill), so an opacity class still applies */\n\t\tanimation: bb-ignite var(--ft-duration-entrance, 600ms)\n\t\t\tvar(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) backwards;\n\t}\n\n\t.border-beam .bb-ride {\n\t\tanimation: bb-travel var(--_dur) linear infinite;\n\t\t/* a piece `lag` turns behind the head is started one lap early, minus its lag,\n\t\t   so it is already running on the first frame */\n\t\tanimation-delay: calc(var(--_offset) - (1 - var(--_lag)) * var(--_dur));\n\t}\n\n\t.border-beam[data-direction=\"reverse\"] .bb-ride {\n\t\tanimation-direction: reverse;\n\t}\n}\n\n@keyframes bb-travel {\n\tfrom {\n\t\toffset-distance: var(--_start);\n\t}\n\tto {\n\t\toffset-distance: calc(var(--_start) + 100%);\n\t}\n}\n\n@keyframes bb-ignite {\n\tfrom {\n\t\topacity: 0;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/border-beam/border-beam.css"},{"path":"react/src/components/border-beam/index.ts","content":"\"use client\";\n\nexport { BorderBeam } from \"./BorderBeam\";\nexport type { BorderBeamProps } from \"./BorderBeam\";\n","type":"registry:ui","target":"@ui/fancy/components/border-beam/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/border-beam?framework=react"}}