{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-beam",
  "title": "Animated Beam",
  "description": "Animated gradient beam that connects two elements with a flowing light path.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/animated-beam/animated-beam.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/** Props for {@link AnimatedBeam}. */\nexport type AnimatedBeamProps = React.ComponentPropsWithoutRef<\"svg\"> & {\n  /** Element that hosts the beam and supplies the coordinate origin. */\n  containerRef: React.RefObject<HTMLElement | null>;\n  /** Curve bow height in pixels. Positive bends up. Defaults to `0`. */\n  curvature?: number;\n  /** Seconds for one travel of the gradient. Defaults to `3`. */\n  duration?: number;\n  /** Element the beam starts from. */\n  fromRef: React.RefObject<HTMLElement | null>;\n  /** Reverse the travel direction of the gradient. */\n  reverse?: boolean;\n  /** Element the beam ends at. */\n  toRef: React.RefObject<HTMLElement | null>;\n};\n\ntype BeamGeometry = { height: number; path: string; width: number };\n\ntype GeometryInput = {\n  container: HTMLElement | null;\n  curvature: number;\n  from: HTMLElement | null;\n  to: HTMLElement | null;\n};\n\nfunction usePrefersReducedMotion(): boolean {\n  const [reduced, setReduced] = React.useState(false);\n\n  React.useEffect(() => {\n    if (\n      typeof window === \"undefined\" ||\n      typeof window.matchMedia !== \"function\"\n    ) {\n      return;\n    }\n\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = (): void => {\n      setReduced(query.matches);\n    };\n\n    onChange();\n    query.addEventListener(\"change\", onChange);\n\n    return () => {\n      query.removeEventListener(\"change\", onChange);\n    };\n  }, []);\n\n  return reduced;\n}\n\nfunction buildGeometry({\n  container,\n  curvature,\n  from,\n  to,\n}: GeometryInput): BeamGeometry {\n  if (container === null || from === null || to === null) {\n    return { height: 0, path: \"\", width: 0 };\n  }\n\n  const base = container.getBoundingClientRect();\n  const start = from.getBoundingClientRect();\n  const end = to.getBoundingClientRect();\n  const startX = start.left - base.left + start.width / 2;\n  const startY = start.top - base.top + start.height / 2;\n  const endX = end.left - base.left + end.width / 2;\n  const endY = end.top - base.top + end.height / 2;\n  const controlX = (startX + endX) / 2;\n  const controlY = (startY + endY) / 2 - curvature;\n  const path = `M ${startX},${startY} Q ${controlX},${controlY} ${endX},${endY}`;\n\n  return { height: base.height, path, width: base.width };\n}\n\nfunction useBeamGeometry(\n  props: Pick<\n    AnimatedBeamProps,\n    \"containerRef\" | \"curvature\" | \"fromRef\" | \"toRef\"\n  >,\n): BeamGeometry {\n  const { containerRef, curvature = 0, fromRef, toRef } = props;\n  const [geometry, setGeometry] = React.useState<BeamGeometry>({\n    height: 0,\n    path: \"\",\n    width: 0,\n  });\n\n  React.useEffect(() => {\n    const container = containerRef.current;\n    const update = (): void => {\n      setGeometry(\n        buildGeometry({\n          container,\n          curvature,\n          from: fromRef.current,\n          to: toRef.current,\n        }),\n      );\n    };\n\n    update();\n\n    if (container === null || typeof ResizeObserver !== \"function\") {\n      return;\n    }\n\n    const observer = new ResizeObserver(update);\n    observer.observe(container);\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [containerRef, curvature, fromRef, toRef]);\n\n  return geometry;\n}\n\n/**\n * Animated gradient beam tracing a curved path between two elements.\n *\n * Respects `prefers-reduced-motion`: the gradient stops travelling.\n *\n * @example\n * ```tsx\n * <AnimatedBeam containerRef={box} fromRef={a} toRef={b} />\n * ```\n */\nexport const AnimatedBeam = ({\n  className,\n  containerRef,\n  curvature = 0,\n  duration = 3,\n  fromRef,\n  ref,\n  reverse = false,\n  toRef,\n  ...props\n}: AnimatedBeamProps & { ref?: React.Ref<SVGSVGElement> }) => {\n  const gradientId = React.useId();\n  const reduced = usePrefersReducedMotion();\n  const { height, path, width } = useBeamGeometry({\n    containerRef,\n    curvature,\n    fromRef,\n    toRef,\n  });\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute inset-0\", className)}\n      fill=\"none\"\n      ref={ref}\n      {...props}\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      width={width}\n    >\n      <path className=\"stroke-border\" d={path} strokeWidth={2} />\n      <path d={path} stroke={`url(#${gradientId})`} strokeWidth={2} />\n      <defs>\n        <linearGradient\n          gradientUnits=\"objectBoundingBox\"\n          id={gradientId}\n          x1=\"0%\"\n          x2=\"20%\"\n        >\n          <stop offset=\"0%\" stopColor=\"oklch(var(--primary) / 0)\" />\n          <stop offset=\"50%\" stopColor=\"oklch(var(--primary))\" />\n          <stop offset=\"100%\" stopColor=\"oklch(var(--primary) / 0)\" />\n          {reduced ? null : (\n            <BeamTravel duration={duration} reverse={reverse} />\n          )}\n        </linearGradient>\n      </defs>\n    </svg>\n  );\n};\nAnimatedBeam.displayName = \"AnimatedBeam\";\n\nfunction BeamTravel({\n  duration,\n  reverse,\n}: {\n  duration: number;\n  reverse: boolean;\n}) {\n  const x1 = reverse ? \"100%;-20%\" : \"-20%;100%\";\n  const x2 = reverse ? \"120%;0%\" : \"0%;120%\";\n\n  return (\n    <>\n      <animate\n        attributeName=\"x1\"\n        dur={`${duration}s`}\n        repeatCount=\"indefinite\"\n        values={x1}\n      />\n      <animate\n        attributeName=\"x2\"\n        dur={`${duration}s`}\n        repeatCount=\"indefinite\"\n        values={x2}\n      />\n    </>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
