Todos os componentes

Grid Beam Background

Grid Beam Background é um componente de efeito para React e Tailwind CSS, da biblioteca Cult UI, com licença MIT. Copia e cola no teu projeto.

Cult UI MIT effects

O que é

Grid Beam Background é um componente de efeito para React e Tailwind CSS, da biblioteca Cult UI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Não precisa de pacotes além do que o projeto já tem

Licença

Este componente vem de Cult UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Grid Beam Background

apps/www/registry/default/ui/grid-beam.tsx
"use client"

import {
  forwardRef,
  useEffect,
  useMemo,
  useRef,
  useState,
  useSyncExternalStore,
  type ComponentProps,
  type CSSProperties,
  type MutableRefObject,
  type RefObject,
} from "react"

import { cn } from "@/lib/utils"

// ─── Color palettes ───────────────────────────────────────────────

export type RGB = readonly [number, number, number]

export type PaletteBand = Readonly<{
  color: RGB
  op: number
}>

export type GridBeamPaletteLayers = Readonly<{
  h: readonly PaletteBand[]
  v: readonly PaletteBand[]
}>

export const PALETTES = {
  colorful: {
    dark: {
      h: [
        { color: [255, 50, 100] as const, op: 0.38 },
        { color: [40, 180, 220] as const, op: 0.35 },
        { color: [50, 200, 80] as const, op: 0.38 },
        { color: [180, 40, 240] as const, op: 0.35 },
        { color: [255, 160, 30] as const, op: 0.38 },
        { color: [100, 70, 255] as const, op: 0.35 },
      ],
      v: [
        { color: [40, 140, 255] as const, op: 0.38 },
        { color: [240, 50, 180] as const, op: 0.35 },
        { color: [30, 185, 170] as const, op: 0.38 },
        { color: [255, 120, 40] as const, op: 0.38 },
        { color: [100, 70, 255] as const, op: 0.35 },
        { color: [50, 200, 80] as const, op: 0.38 },
      ],
    },
    light: {
      h: [
        { color: [200, 30, 70] as const, op: 0.28 },
        { color: [20, 140, 180] as const, op: 0.24 },
        { color: [30, 160, 50] as const, op: 0.28 },
        { color: [140, 20, 200] as const, op: 0.24 },
        { color: [210, 120, 10] as const, op: 0.28 },
        { color: [70, 40, 210] as const, op: 0.24 },
      ],
      v: [
        { color: [20, 100, 210] as const, op: 0.28 },
        { color: [200, 30, 140] as const, op: 0.24 },
        { color: [15, 145, 130] as const, op: 0.28 },
        { color: [210, 90, 20] as const, op: 0.28 },
        { color: [70, 40, 210] as const, op: 0.24 },
        { color: [30, 160, 50] as const, op: 0.28 },
      ],
    },
  },
  mono: {
    dark: {
      h: [
        { color: [200, 200, 200] as const, op: 0.16 },
        { color: [180, 180, 180] as const, op: 0.13 },
        { color: [190, 190, 190] as const, op: 0.16 },
        { color: [175, 175, 175] as const, op: 0.13 },
        { color: [195, 195, 195] as const, op: 0.16 },
        { color: [185, 185, 185] as const, op: 0.13 },
      ],
      v: [
        { color: [185, 185, 185] as const, op: 0.16 },
        { color: [170, 170, 170] as const, op: 0.13 },
        { color: [195, 195, 195] as const, op: 0.16 },
        { color: [180, 180, 180] as const, op: 0.13 },
        { color: [190, 190, 190] as const, op: 0.16 },
        { color: [175, 175, 175] as const, op: 0.13 },
      ],
    },
    light: {
      h: [
        { color: [90, 90, 90] as const, op: 0.13 },
        { color: [110, 110, 110] as const, op: 0.1 },
        { color: [80, 80, 80] as const, op: 0.13 },
        { color: [100, 100, 100] as const, op: 0.1 },
        { color: [85, 85, 85] as const, op: 0.13 },
        { color: [95, 95, 95] as const, op: 0.1 },
      ],
      v: [
        { color: [100, 100, 100] as const, op: 0.13 },
        { color: [80, 80, 80] as const, op: 0.1 },
        { color: [90, 90, 90] as const, op: 0.13 },
        { color: [110, 110, 110] as const, op: 0.1 },
        { color: [85, 85, 85] as const, op: 0.13 },
        { color: [95, 95, 95] as const, op: 0.1 },
      ],
    },
  },
  ocean: {
    dark: {
      h: [
        { color: [100, 80, 255] as const, op: 0.38 },
        { color: [60, 140, 255] as const, op: 0.35 },
        { color: [80, 100, 220] as const, op: 0.38 },
        { color: [130, 70, 255] as const, op: 0.35 },
        { color: [50, 120, 230] as const, op: 0.38 },
        { color: [110, 90, 240] as const, op: 0.35 },
      ],
      v: [
        { color: [70, 130, 255] as const, op: 0.38 },
        { color: [120, 80, 240] as const, op: 0.35 },
        { color: [90, 110, 230] as const, op: 0.38 },
        { color: [60, 100, 255] as const, op: 0.35 },
        { color: [140, 100, 255] as const, op: 0.38 },
        { color: [80, 120, 220] as const, op: 0.35 },
      ],
    },
    light: {
      h: [
        { color: [60, 50, 180] as const, op: 0.28 },
        { color: [40, 100, 210] as const, op: 0.24 },
        { color: [50, 70, 190] as const, op: 0.28 },
        { color: [90, 50, 200] as const, op: 0.24 },
        { color: [30, 80, 200] as const, op: 0.28 },
        { color: [70, 60, 210] as const, op: 0.24 },
      ],
      v: [
        { color: [50, 90, 220] as const, op: 0.28 },
        { color: [80, 60, 200] as const, op: 0.24 },
        { color: [60, 80, 210] as const, op: 0.28 },
        { color: [40, 70, 200] as const, op: 0.24 },
        { color: [100, 80, 230] as const, op: 0.28 },
        { color: [55, 85, 195] as const, op: 0.24 },
      ],
    },
  },
  sunset: {
    dark: {
      h: [
        { color: [255, 100, 60] as const, op: 0.38 },
        { color: [255, 180, 50] as const, op: 0.35 },
        { color: [255, 80, 80] as const, op: 0.38 },
        { color: [255, 140, 40] as const, op: 0.35 },
        { color: [255, 200, 60] as const, op: 0.38 },
        { color: [255, 120, 50] as const, op: 0.35 },
      ],
      v: [
        { color: [255, 160, 70] as const, op: 0.38 },
        { color: [255, 90, 60] as const, op: 0.35 },
        { color: [255, 200, 50] as const, op: 0.38 },
        { color: [255, 70, 70] as const, op: 0.35 },
        { color: [255, 150, 40] as const, op: 0.38 },
        { color: [255, 100, 80] as const, op: 0.35 },
      ],
    },
    light: {
      h: [
        { color: [200, 70, 30] as const, op: 0.28 },
        { color: [210, 140, 20] as const, op: 0.24 },
        { color: [190, 50, 50] as const, op: 0.28 },
        { color: [200, 100, 20] as const, op: 0.24 },
        { color: [220, 160, 30] as const, op: 0.28 },
        { color: [195, 80, 25] as const, op: 0.24 },
      ],
      v: [
        { color: [210, 120, 40] as const, op: 0.28 },
        { color: [190, 60, 40] as const, op: 0.24 },
        { color: [220, 160, 25] as const, op: 0.28 },
        { color: [185, 45, 45] as const, op: 0.24 },
        { color: [200, 110, 20] as const, op: 0.28 },
        { color: [195, 75, 50] as const, op: 0.24 },
      ],
    },
  },
} as const satisfies Record<
  string,
  Record<"dark" | "light", GridBeamPaletteLayers>
>

export type GridBeamPaletteKey = keyof typeof PALETTES

export type GridBeamColorScheme = "dark" | "light"

export type GridBeamThemeProp = GridBeamColorScheme | "auto"

function subscribePreferredColorScheme(onChange: () => void) {
  const mq = window.matchMedia("(prefers-color-scheme: dark)")
  mq.addEventListener("change", onChange)
  return () => mq.removeEventListener("change", onChange)
}

function getPreferredColorSchemeSnapshot(): GridBeamColorScheme {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light"
}

function useResolvedColorScheme(theme: GridBeamThemeProp): GridBeamColorScheme {
  const systemScheme = useSyncExternalStore(
    subscribePreferredColorScheme,
    getPreferredColorSchemeSnapshot,
    () => "light" as GridBeamColorScheme
  )
  if (theme === "auto") {
    return systemScheme
  }
  return theme
}

export function resolveGridBeamPalette(
  colorVariant: GridBeamPaletteKey,
  scheme: GridBeamColorScheme
): GridBeamPaletteLayers {
  const variant = PALETTES[colorVariant]
  if (!variant) {
    return PALETTES.colorful.dark
  }
  return variant[scheme] ?? PALETTES.colorful.dark
}

function smoothstep(t: number): number {
  return t * t * (3 - 2 * t)
}

function gaussian(x: number, s: number): number {
  return Math.exp(-(x * x) / (2 * s * s))
}

export type BeamCanvasRuntimeConfig = Readonly<{
  rows: number
  cols: number
  palette: GridBeamPaletteLayers
  active: boolean
  fadingOut: boolean
  fadeStart: number | null
  duration: number
  strength: number
  breathe: boolean
}>

function useBeamCanvas(
  canvasRef: RefObject<HTMLCanvasElement | null>,
  config: MutableRefObject<BeamCanvasRuntimeConfig>
) {
  const animRef = useRef<number | null>(null)
  const startRef = useRef<number | null>(null)

  // biome-ignore lint/correctness/useExhaustiveDependencies: animation reads latest `config.current` each frame; only canvas mount should restart the loop.
  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) {
      return
    }
    const ctx = canvas.getContext("2d", { alpha: true })
    if (!ctx) {
      return
    }
    const dpr = window.devicePixelRatio || 1

    const resize = () => {
      const rect = canvas.getBoundingClientRect()
      canvas.width = rect.width * dpr
      canvas.height = rect.height * dpr
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
    }
    resize()
    const ro = new ResizeObserver(resize)
    ro.observe(canvas)
    startRef.current = performance.now()

    // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: single canvas frame renderer; splitting would obscure the math.
    const draw = (now: number) => {
      const {
        rows,
        cols,
        palette,
        active,
        fadingOut,
        fadeStart,
        duration,
        strength,
        breathe,
      } = config.current
      const rect = canvas.getBoundingClientRect()
      const w = rect.width
      const h = rect.height
      ctx.clearRect(0, 0, w, h)

      if (!(active || fadingOut)) {
        animRef.current = requestAnimationFrame(draw)
        return
      }

      const elapsed = (now - (startRef.current ?? now)) / 1000
      let fade = 1
      if (fadingOut && fadeStart) {
        fade = Math.max(0, 1 - (now - fadeStart) / 600)
        if (fade <= 0) {
          animRef.current = requestAnimationFrame(draw)
          return
        }
      } else if (active) {
        fade = smoothstep(Math.min(1, elapsed / 0.8))
      }

      const cellW = w / cols
      const cellH = h / rows
      const gs = fade * strength
      const br = breathe
        ? 0.85 + 0.3 * Math.sin(elapsed * 1.4) + 0.1 * Math.sin(elapsed * 2.3)
        : 1

      const rgba = (r: number, g: number, b: number, a: number) =>
        `rgba(${r},${g},${b},${Math.max(0, a).toFixed(4)})`

      for (let r = 1; r < rows; r++) {
        const y = r * cellH
        const pal = palette.h[r % palette.h.length]
        const [cr, cg, cb] = pal.color
        const op = pal.op
        const speed = 1 + (r % 3) * 0.12
        const offset = r * 0.21 + (r % 2) * 0.35
        const t = ((elapsed * speed) / duration + offset) % 1
        const x = t * w

        const bloomLen = cellW * 0.6 * br
        const bloomH = 4
        const bloomGrad = ctx.createRadialGradient(x, y, 0, x, y, bloomLen)
        bloomGrad.addColorStop(0, rgba(cr, cg, cb, op * 0.3 * gs))
        bloomGrad.addColorStop(0.4, rgba(cr, cg, cb, op * 0.12 * gs))
        bloomGrad.addColorStop(1, "transparent")
        ctx.save()
        ctx.scale(1, bloomH / bloomLen)
        ctx.fillStyle = bloomGrad
        ctx.beginPath()
        ctx.arc(x, (y * bloomLen) / bloomH, bloomLen, 0, Math.PI * 2)
        ctx.fill()
        ctx.restore()

        const coreLen = cellW * 0.55 * br
        const lineGrad = ctx.createLinearGradient(
          x - coreLen,
          y,
          x + coreLen,
          y
        )
        lineGrad.addColorStop(0, "transparent")
        lineGrad.addColorStop(0.12, rgba(cr, cg, cb, op * 0.4 * gs))
        lineGrad.addColorStop(
          0.35,
          rgba(
            Math.min(255, cr + 60),
            Math.min(255, cg + 60),
            Math.min(255, cb + 60),
            op * 0.8 * gs
          )
        )
        lineGrad.addColorStop(
          0.5,
          rgba(
            Math.min(255, cr + 100),
            Math.min(255, cg + 100),
            Math.min(255, cb + 100),
            op * 1.0 * gs
          )
        )
        lineGrad.addColorStop(
          0.65,
          rgba(
            Math.min(255, cr + 60),
            Math.min(255, cg + 60),
            Math.min(255, cb + 60),
            op * 0.8 * gs
          )
        )
        lineGrad.addColorStop(0.88, rgba(cr, cg, cb, op * 0.4 * gs))
        lineGrad.addColorStop(1, "transparent")
        ctx.strokeStyle = lineGrad
        ctx.lineWidth = 1.5
        ctx.beginPath()
        ctx.moveTo(x - coreLen, y)
        ctx.lineTo(x + coreLen, y)
        ctx.stroke()
      }

      for (let c = 1; c < cols; c++) {
        const x = c * cellW
        const pal = palette.v[c % palette.v.length]
        const [cr, cg, cb] = pal.color
        const op = pal.op
        const speed = 1 + (c % 3) * 0.1
        const offset = c * 0.26 + (c % 2) * 0.4
        const t = ((elapsed * speed) / (duration * 1.2) + offset) % 1
        const y = t * h

        const bloomLen = cellH * 0.6 * br
        const bloomW = 4
        const bloomGrad = ctx.createRadialGradient(x, y, 0, x, y, bloomLen)
        bloomGrad.addColorStop(0, rgba(cr, cg, cb, op * 0.3 * gs))
        bloomGrad.addColorStop(0.4, rgba(cr, cg, cb, op * 0.12 * gs))
        bloomGrad.addColorStop(1, "transparent")
        ctx.save()
        ctx.scale(bloomW / bloomLen, 1)
        ctx.fillStyle = bloomGrad
        ctx.beginPath()
        ctx.arc((x * bloomLen) / bloomW, y, bloomLen, 0, Math.PI * 2)
        ctx.fill()
        ctx.restore()

        const coreLen = cellH * 0.55 * br
        const lineGrad = ctx.createLinearGradient(
          x,
          y - coreLen,
          x,
          y + coreLen
        )
        lineGrad.addColorStop(0, "transparent")
        lineGrad.addColorStop(0.12, rgba(cr, cg, cb, op * 0.4 * gs))
        lineGrad.addColorStop(
          0.35,
          rgba(
            Math.min(255, cr + 60),
            Math.min(255, cg + 60),
            Math.min(255, cb + 60),
            op * 0.8 * gs
          )
        )
        lineGrad.addColorStop(
          0.5,
          rgba(
            Math.min(255, cr + 100),
            Math.min(255, cg + 100),
            Math.min(255, cb + 100),
            op * 1.0 * gs
          )
        )
        lineGrad.addColorStop(
          0.65,
          rgba(
            Math.min(255, cr + 60),
            Math.min(255, cg + 60),
            Math.min(255, cb + 60),
            op * 0.8 * gs
          )
        )
        lineGrad.addColorStop(0.88, rgba(cr, cg, cb, op * 0.4 * gs))
        lineGrad.addColorStop(1, "transparent")
        ctx.strokeStyle = lineGrad
        ctx.lineWidth = 1.5
        ctx.beginPath()
        ctx.moveTo(x, y - coreLen)
        ctx.lineTo(x, y + coreLen)
        ctx.stroke()
      }

      for (let r = 1; r < rows; r++) {
        for (let c = 1; c < cols; c++) {
          const ix = c * cellW
          const iy = r * cellH
          const hSpeed = 1 + (r % 3) * 0.12
          const hOffset = r * 0.21 + (r % 2) * 0.35
          const ht = ((elapsed * hSpeed) / duration + hOffset) % 1
          const hx = ht * w
          const vSpeed = 1 + (c % 3) * 0.1
          const vOffset = c * 0.26 + (c % 2) * 0.4
          const vt = ((elapsed * vSpeed) / (duration * 1.2) + vOffset) % 1
          const vy = vt * h

          const proxH = gaussian((hx - ix) / cellW, 0.25)
          const proxV = gaussian((vy - iy) / cellH, 0.25)
          const prox = proxH * proxV

          if (prox > 0.05) {
            const pH = palette.h[r % palette.h.length]
            const pV = palette.v[c % palette.v.length]
            const mr = Math.floor((pH.color[0] + pV.color[0]) / 2)
            const mg = Math.floor((pH.color[1] + pV.color[1]) / 2)
            const mb = Math.floor((pH.color[2] + pV.color[2]) / 2)
            const fr = 3.5 * Math.sqrt(prox)
            const fop = prox * 0.6 * gs

            const fg = ctx.createRadialGradient(ix, iy, 0, ix, iy, fr)
            fg.addColorStop(
              0,
              rgba(
                Math.min(255, mr + 140),
                Math.min(255, mg + 140),
                Math.min(255, mb + 140),
                fop
              )
            )
            fg.addColorStop(0.5, rgba(mr, mg, mb, fop * 0.4))
            fg.addColorStop(1, "transparent")
            ctx.fillStyle = fg
            ctx.beginPath()
            ctx.arc(ix, iy, fr, 0, Math.PI * 2)
            ctx.fill()
          }
        }
      }

      animRef.current = requestAnimationFrame(draw)
    }

    animRef.current = requestAnimationFrame(draw)
    return () => {
      if (animRef.current !== null) {
        cancelAnimationFrame(animRef.current)
      }
      ro.disconnect()
    }
  }, [canvasRef])
}

export type UseGridBeamOptions = Readonly<{
  rows?: number
  cols?: number
  colorVariant?: GridBeamPaletteKey
  /** Resolved automatically when `"auto"` via `prefers-color-scheme`. */
  theme?: GridBeamThemeProp
  active?: boolean
  duration?: number
  strength?: number
  breathe?: boolean
}>

export type UseGridBeamResult = Readonly<{
  canvasRef: RefObject<HTMLCanvasElement | null>
  palette: GridBeamPaletteLayers
  resolvedScheme: GridBeamColorScheme
  rows: number
  cols: number
  fadingOut: boolean
  fadeStart: number | null
}>

/**
 * Headless state for the grid beam canvas: palette resolution, fade-out when `active` becomes false,
 * and a ref to attach to {@link GridBeamCanvas}.
 */
export function useGridBeam({
  rows: rowsProp = 3,
  cols: colsProp = 4,
  colorVariant = "colorful",
  theme = "dark",
  active = true,
  duration = 3,
  strength = 1,
  breathe = true,
}: UseGridBeamOptions): UseGridBeamResult {
  const rows = Math.max(2, rowsProp)
  const cols = Math.max(2, colsProp)

  const canvasRef = useRef<HTMLCanvasElement>(null)
  const [fadingOut, setFadingOut] = useState(false)
  const [fadeStart, setFadeStart] = useState<number | null>(null)
  const prevActive = useRef(active)

  const resolvedScheme = useResolvedColorScheme(theme)

  const palette = useMemo(
    () => resolveGridBeamPalette(colorVariant, resolvedScheme),
    [colorVariant, resolvedScheme]
  )

  useEffect(() => {
    if (prevActive.current && !active) {
      setFadingOut(true)
      setFadeStart(performance.now())
      const timer = window.setTimeout(() => setFadingOut(false), 700)
      prevActive.current = active
      return () => window.clearTimeout(timer)
    }
    prevActive.current = active
  }, [active])

  const configRef = useRef<BeamCanvasRuntimeConfig>({
    rows,
    cols,
    palette,
    active,
    fadingOut,
    fadeStart,
    duration,
    strength,
    breathe,
  })
  configRef.current = {
    rows,
    cols,
    palette,
    active,
    fadingOut,
    fadeStart,
    duration,
    strength,
    breathe,
  }

  useBeamCanvas(canvasRef, configRef)

  return {
    canvasRef,
    palette,
    resolvedScheme,
    rows,
    cols,
    fadingOut,
    fadeStart,
  }
}

export type GridBeamDividersProps = ComponentProps<"svg"> &
  Readonly<{
    rows: number
    cols: number
    /** Grid line color (default `var(--border)` from shadcn theme). */
    dividerStroke?: string
  }>

/** SVG grid lines only — pair with {@link GridBeamCanvas} and your own layout. */
export function GridBeamDividers({
  rows,
  cols,
  dividerStroke = "var(--border)",
  className,
  ...props
}: GridBeamDividersProps) {
  return (
    <svg
      aria-hidden
      className={cn(
        "pointer-events-none absolute inset-0 z-1 h-full w-full",
        className
      )}
      preserveAspectRatio="none"
      role="presentation"
      {...props}
    >
      {Array.from({ length: rows - 1 }, (_, r) => {
        const y = `${((r + 1) / rows) * 100}%`
        return (
          <line
            key={`h-${rows}-${y}`}
            stroke={dividerStroke}
            strokeWidth={1}
            x1="0"
            x2="100%"
            y1={y}
            y2={y}
          />
        )
      })}
      {Array.from({ length: cols - 1 }, (_, c) => {
        const x = `${((c + 1) / cols) * 100}%`
        return (
          <line
            key={`v-${cols}-${x}`}
            stroke={dividerStroke}
            strokeWidth={1}
            x1={x}
            x2={x}
            y1="0"
            y2="100%"
          />
        )
      })}
    </svg>
  )
}

export type GridBeamCanvasProps = ComponentProps<"canvas"> &
  Readonly<{
    borderRadius?: number
  }>

export const GridBeamCanvas = forwardRef<
  HTMLCanvasElement,
  GridBeamCanvasProps
>(function GridBeamCanvas({ className, style, borderRadius, ...props }, ref) {
  return (
    <canvas
      aria-hidden
      className={cn(
        "pointer-events-none absolute inset-0 z-2 h-full w-full",
        className
      )}
      ref={ref}
      style={{ borderRadius, ...style } as CSSProperties}
      {...props}
    />
  )
})

export type GridBeamContentProps = ComponentProps<"div">

/** Slot above the beam layers — default `z-index` stacks content on top. */
export function GridBeamContent({ className, ...props }: GridBeamContentProps) {
  return (
    <div
      className={cn("relative z-3", className)}
      data-slot="grid-beam-content"
      {...props}
    />
  )
}

export type GridBeamProps = UseGridBeamOptions &
  ComponentProps<"div"> &
  Readonly<{
    /** Corner radius for the root and canvas (divider SVG is rectangular). */
    borderRadius?: number
  }>

/**
 * Opinionated composition: root + dividers + canvas + content slot.
 * For full control, call {@link useGridBeam} and assemble {@link GridBeamDividers}, {@link GridBeamCanvas}, {@link GridBeamContent} yourself.
 */
export function GridBeam({
  children,
  className,
  style,
  borderRadius,
  rows,
  cols,
  colorVariant,
  theme,
  active,
  duration,
  strength,
  breathe,
  ...props
}: GridBeamProps) {
  const {
    canvasRef,
    rows: r,
    cols: c,
  } = useGridBeam({
    rows,
    cols,
    colorVariant,
    theme,
    active,
    duration,
    strength,
    breathe,
  })

  return (
    <div
      className={cn("relative overflow-hidden", className)}
      data-slot="grid-beam"
      style={{ borderRadius, ...style }}
      {...props}
    >
      <GridBeamDividers cols={c} rows={r} />
      <GridBeamCanvas borderRadius={borderRadius} ref={canvasRef} />
      <GridBeamContent>{children}</GridBeamContent>
    </div>
  )
}

Componentes parecidos