{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-beam",
  "title": "Animated Beam",
  "description": "A line of light running between two real elements, measured so it follows every reflow: straight, bowed, stepped like a circuit or many sources into one hub.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/animated-beam.tsx",
      "content": "export { AnimatedBeam, type AnimatedBeamProps, type AnimatedBeamEffect, type AnimatedBeamVariant } from \"@/registry/miseui/ui/animated-beam-line\"\r\nexport { AnimatedBeamHub, type AnimatedBeamHubProps, type HubNode } from \"@/registry/miseui/ui/animated-beam-hub\"\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/animated-beam-hub.tsx",
      "content": "import type * as React from \"react\"\r\nimport { createRef, useMemo, useRef } from \"react\"\r\nimport { AnimatedBeam, type AnimatedBeamEffect } from \"@/registry/miseui/ui/animated-beam-line\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ninterface HubNode {\r\n  id: string\r\n  /** Names it for assistive tech. The tile itself shows only the icon. */\r\n  label: string\r\n  icon: React.ReactNode\r\n}\r\n\r\ntype AnimatedBeamHubProps = Omit<React.ComponentProps<\"div\">, \"ref\" | \"children\"> & {\r\n  /** What flows in. Beyond about five, the row wraps in a narrow container. */\r\n  sources: readonly HubNode[]\r\n  center: HubNode\r\n  effect?: AnimatedBeamEffect\r\n  /** Seconds of one cycle per beam. */\r\n  duration?: number\r\n  paused?: boolean\r\n  reverse?: boolean\r\n  /** With `pulse`, a new value sends one light down every beam. */\r\n  trigger?: unknown\r\n  /** Names the whole picture for assistive tech. */\r\n  label?: string\r\n}\r\n\r\nconst TILE_CLASS = \"flex items-center justify-center rounded-xl border bg-background text-foreground shadow-sm [&_svg]:size-4 [&_svg]:shrink-0\"\r\n\r\n/** Stacks in a narrow container and lies down when there is room. The beams need no telling: they are measured, and read the layout. */\r\nfunction AnimatedBeamHub({ sources, center, effect, duration = 4, paused = false, reverse = false, trigger, label, className, ...props }: AnimatedBeamHubProps) {\r\n  const rootRef = useRef<HTMLDivElement>(null)\r\n  const centerRef = useRef<HTMLDivElement>(null)\r\n  const sourceRefs = useMemo(() => sources.map(() => createRef<HTMLDivElement>()), [sources])\r\n  const stagger = duration / Math.max(sources.length, 1)\r\n  return (\r\n    <div\r\n      {...props}\r\n      ref={rootRef}\r\n      role=\"group\"\r\n      aria-label={label}\r\n      data-slot=\"animated-beam-hub\"\r\n      data-variant=\"hub\"\r\n      data-effect={effect}\r\n      className={cn(\"@container relative isolate w-full min-w-0\", className)}\r\n    >\r\n      <div className=\"flex flex-col items-center gap-12 @md:flex-row @md:justify-center @md:gap-24\">\r\n        <ul data-slot=\"animated-beam-hub-sources\" className=\"grid w-full grid-cols-[repeat(auto-fit,minmax(40px,1fr))] justify-items-center gap-y-3 @md:w-auto @md:grid-cols-1\">\r\n          {sources.map((source, index) => (\r\n            <li key={source.id}>\r\n              <div ref={sourceRefs[index]} role=\"img\" aria-label={source.label} className={cn(TILE_CLASS, \"size-10\")}>\r\n                {source.icon}\r\n              </div>\r\n            </li>\r\n          ))}\r\n        </ul>\r\n        <div ref={centerRef} role=\"img\" aria-label={center.label} data-slot=\"animated-beam-hub-center\" className={cn(TILE_CLASS, \"size-12 rounded-2xl border-transparent bg-mise text-mise-foreground ring-1 ring-foreground/10\")}>\r\n          {center.icon}\r\n        </div>\r\n      </div>\r\n      {sources.map((source, index) => (\r\n        <AnimatedBeam\r\n          key={source.id}\r\n          containerRef={rootRef}\r\n          fromRef={sourceRefs[index] as React.RefObject<HTMLDivElement | null>}\r\n          toRef={centerRef}\r\n          variant=\"hub\"\r\n          effect={effect}\r\n          reverse={reverse}\r\n          duration={duration}\r\n          delay={index * stagger}\r\n          paused={paused}\r\n          trigger={trigger}\r\n        />\r\n      ))}\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { AnimatedBeamHub, type AnimatedBeamHubProps, type HubNode }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/animated-beam-line.tsx",
      "content": "import type * as React from \"react\"\r\nimport { useEffect, useState } from \"react\"\r\nimport { useReducedMotion } from \"motion/react\"\r\nimport { LIGHT_TOKEN_CLASS, LOOP_CLASS, loopStyle, playStyle, useBackgroundPlayback } from \"@/registry/miseui/ui/background-loop\"\r\nimport { beamPath, boxWithin, type BeamVariant } from \"@/registry/miseui/ui/animated-beam-math\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ntype AnimatedBeamEffect = \"pulse\"\r\ntype BeamRef = React.RefObject<HTMLElement | null>\r\n\r\n/**\r\n * Dashes on the path itself, not an element sliding along it. A rigid light cannot bend: at a corner its tail sticks out\r\n * past the line. This repaints the path's box each frame instead of moving a layer, and it is the one place the kit\r\n * animates something other than transform and opacity. The dash starts and ends just off the path, because a dash of\r\n * zero length on the very end still draws its round cap. Offsets carry px because some browsers refuse a bare number in\r\n * this property; with pathLength 1 a px is the whole path.\r\n */\r\nconst KEYFRAMES = [\r\n  \"@keyframes mise-beam-run{0%{stroke-dashoffset:var(--mise-dash-from)}70%,100%{stroke-dashoffset:-1.02px}}\",\r\n  \"@keyframes mise-beam-once{0%{stroke-dashoffset:var(--mise-dash-from)}100%{stroke-dashoffset:-1.02px}}\",\r\n  \"@keyframes mise-beam-flash{0%{opacity:0}25%{opacity:1}100%{opacity:0}}\",\r\n].join(\"\")\r\nconst TRAVEL_EASE = \"cubic-bezier(0.45, 0, 0.55, 1)\"\r\nconst MAX_PULSES = 3\r\n/** Three lengths with one head give a tail that fades along the curve, where a gradient along a stroke does not exist in SVG. */\r\nconst LIGHT_PARTS = [\r\n  { dash: 0.24, opacity: 0.3 },\r\n  { dash: 0.13, opacity: 0.55 },\r\n  { dash: 0.05, opacity: 1 },\r\n] as const\r\n\r\ninterface BeamLightProps {\r\n  path: string\r\n  animation: () => React.CSSProperties\r\n  className?: string\r\n  onDone?: () => void\r\n}\r\n\r\nfunction BeamLight({ path, animation, className, onDone }: BeamLightProps) {\r\n  return (\r\n    <g data-slot=\"animated-beam-light\" onAnimationEnd={onDone}>\r\n      {LIGHT_PARTS.map(({ dash, opacity }) => (\r\n        <path\r\n          key={dash}\r\n          d={path}\r\n          pathLength={1}\r\n          fill=\"none\"\r\n          stroke=\"var(--mise-light)\"\r\n          strokeWidth={2}\r\n          strokeLinecap=\"round\"\r\n          strokeOpacity={opacity}\r\n          strokeDasharray={`${dash} 3`}\r\n          strokeDashoffset={dash + 0.02}\r\n          className={className}\r\n          style={{ \"--mise-dash-from\": `${dash + 0.02}px`, ...animation() } as React.CSSProperties}\r\n        />\r\n      ))}\r\n    </g>\r\n  )\r\n}\r\n\r\n/**\r\n * Passive, not layout: a parent's ref attaches after its children's layout effects run, so a beam placed first inside\r\n * its container would find nothing there. It draws nothing until measured, and nothing around it depends on it.\r\n */\r\nfunction useBeamPath(containerRef: BeamRef, fromRef: BeamRef, toRef: BeamRef, variant: BeamVariant, curvature: number, reverse: boolean) {\r\n  const [path, setPath] = useState<string | null>(null)\r\n  useEffect(() => {\r\n    const container = containerRef.current\r\n    const from = fromRef.current\r\n    const to = toRef.current\r\n    if (!container || !from || !to) return\r\n    let live = true\r\n    const measure = () => {\r\n      if (!live) return\r\n      const frame = container.getBoundingClientRect()\r\n      const box = (element: HTMLElement) => boxWithin(element.getBoundingClientRect(), frame, container.clientLeft, container.clientTop)\r\n      setPath(beamPath(box(from), box(to), variant, curvature, reverse))\r\n    }\r\n    const resize = new ResizeObserver(measure)\r\n    // Parents too: a sibling growing moves an end without resizing the end or a fixed-height container.\r\n    for (const element of new Set([container, from, to, from.parentElement, to.parentElement])) {\r\n      if (element) resize.observe(element)\r\n    }\r\n    const changes = new MutationObserver(measure)\r\n    changes.observe(container, { childList: true, subtree: true })\r\n    void document.fonts?.ready.then(measure)\r\n    return () => {\r\n      live = false\r\n      resize.disconnect()\r\n      changes.disconnect()\r\n    }\r\n  }, [containerRef, fromRef, toRef, variant, curvature, reverse])\r\n  return path\r\n}\r\n\r\n/** Render-phase update: one light per real change of the prop, and none for the value it mounted with. */\r\nfunction usePulses(trigger: unknown, enabled: boolean) {\r\n  const [seen, setSeen] = useState(trigger)\r\n  const [nextId, setNextId] = useState(0)\r\n  const [pulses, setPulses] = useState<readonly number[]>([])\r\n  if (trigger !== seen) {\r\n    setSeen(trigger)\r\n    if (enabled) {\r\n      setNextId(nextId + 1)\r\n      setPulses([...pulses.slice(1 - MAX_PULSES), nextId])\r\n    }\r\n  }\r\n  const finish = (id: number) => setPulses((current) => current.filter((pulse) => pulse !== id))\r\n  return { pulses, finish }\r\n}\r\n\r\ntype AnimatedBeamProps = Omit<React.ComponentProps<\"div\">, \"ref\" | \"children\"> & {\r\n  /** Holds the beam and both ends. Needs `relative isolate`, so the beam can sit behind what is inside it. */\r\n  containerRef: BeamRef\r\n  fromRef: BeamRef\r\n  toRef: BeamRef\r\n  /** `line` straight, `curve` bowed by `curvature`, `step` right angles with rounded corners, `hub` leaves and arrives square, as AnimatedBeamHub draws. */\r\n  variant?: BeamVariant\r\n  /** `pulse` sends one light per change of `trigger` instead of a loop, for something that really happened. */\r\n  effect?: AnimatedBeamEffect\r\n  /** Pixels the `curve` bows. Negative bows the other way. */\r\n  curvature?: number\r\n  reverse?: boolean\r\n  /** Seconds of one cycle. */\r\n  duration?: number\r\n  /** Seconds before it starts, to offset several beams from each other. */\r\n  delay?: number\r\n  /** Holds the loop. Wire it to a control: motion past five seconds needs a way to stop it. */\r\n  paused?: boolean\r\n  /** With `pulse`, any new value sends one light. */\r\n  trigger?: unknown\r\n}\r\n\r\nfunction AnimatedBeam({ containerRef, fromRef, toRef, variant = \"line\", effect, curvature = 40, reverse = false, duration = 3.2, delay = 0, paused = false, trigger, className, style, ...props }: AnimatedBeamProps) {\r\n  const pulsing = effect === \"pulse\"\r\n  const path = useBeamPath(containerRef, fromRef, toRef, variant, curvature, reverse)\r\n  const { still, playing } = useBackgroundPlayback(containerRef, { paused, loops: !pulsing })\r\n  const { pulses, finish } = usePulses(trigger, pulsing)\r\n  const reducedMotion = useReducedMotion() ?? false\r\n  return (\r\n    <div\r\n      {...props}\r\n      aria-hidden=\"true\"\r\n      data-slot=\"animated-beam\"\r\n      data-variant={variant}\r\n      data-effect={effect}\r\n      data-playing={still ? undefined : String(playing)}\r\n      className={cn(\"pointer-events-none absolute inset-0 -z-10\", LIGHT_TOKEN_CLASS, className)}\r\n      style={{ ...playStyle(playing), ...style }}\r\n    >\r\n      <style href=\"mise-animated-beam\" precedence=\"default\">{KEYFRAMES}</style>\r\n      {path ? (\r\n        <svg data-slot=\"animated-beam-svg\" className=\"absolute inset-0 size-full overflow-visible\">\r\n          <path data-slot=\"animated-beam-base\" d={path} fill=\"none\" stroke=\"var(--border)\" strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n          {!still && !pulsing ? <BeamLight path={path} className={LOOP_CLASS} animation={() => loopStyle(\"mise-beam-run\", duration, delay, TRAVEL_EASE)} /> : null}\r\n          {pulses.map((id) =>\r\n            reducedMotion ? (\r\n              // Nothing moves, and the line still answers: a fade says something happened without travelling anywhere.\r\n              <path\r\n                key={id}\r\n                data-slot=\"animated-beam-flash\"\r\n                d={path}\r\n                fill=\"none\"\r\n                stroke=\"var(--mise-light)\"\r\n                strokeWidth={1.5}\r\n                strokeLinecap=\"round\"\r\n                style={{ animation: \"mise-beam-flash 900ms var(--ease-mise) both\" }}\r\n                onAnimationEnd={() => finish(id)}\r\n              />\r\n            ) : (\r\n              <BeamLight key={id} path={path} animation={() => ({ animation: `mise-beam-once ${duration * 0.7}s ${TRAVEL_EASE} both` })} onDone={() => finish(id)} />\r\n            ),\r\n          )}\r\n        </svg>\r\n      ) : null}\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { AnimatedBeam, type AnimatedBeamProps, type AnimatedBeamEffect, type BeamVariant as AnimatedBeamVariant }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/animated-beam-math.ts",
      "content": "export type BeamVariant = \"line\" | \"curve\" | \"step\" | \"hub\"\n\nexport interface BeamBox {\n  x: number\n  y: number\n  width: number\n  height: number\n}\n\ninterface RectLike {\n  left: number\n  top: number\n  width: number\n  height: number\n}\n\nexport interface BeamEnds {\n  startX: number\n  startY: number\n  endX: number\n  endY: number\n  /** Which way the two boxes mostly face each other. */\n  axis: \"x\" | \"y\"\n}\n\nconst STEP_RADIUS = 12\nconst STRAIGHT_BELOW_PX = 1\n\n/** clientLeft and clientTop: the svg sits inside the container's border, its rect does not. */\nexport function boxWithin(rect: RectLike, container: RectLike, borderLeft = 0, borderTop = 0): BeamBox {\n  return { x: rect.left - container.left - borderLeft, y: rect.top - container.top - borderTop, width: rect.width, height: rect.height }\n}\n\nexport function isDrawable(from: BeamBox, to: BeamBox) {\n  return from.width > 0 && from.height > 0 && to.width > 0 && to.height > 0\n}\n\n/**\n * Runs edge to edge across the larger clearance, so the line leaves each box from the side facing the other one. A\n * stacked layout gets vertical beams and a row gets horizontal ones, from the same call, which is what lets a beam\n * follow a reflow instead of cutting through a box.\n */\nexport function beamEnds(from: BeamBox, to: BeamBox): BeamEnds {\n  const fromX = from.x + from.width / 2\n  const fromY = from.y + from.height / 2\n  const toX = to.x + to.width / 2\n  const toY = to.y + to.height / 2\n  const clearX = Math.abs(toX - fromX) - (from.width + to.width) / 2\n  const clearY = Math.abs(toY - fromY) - (from.height + to.height) / 2\n  if (clearX >= clearY) {\n    const right = toX >= fromX ? 1 : -1\n    return { startX: fromX + (right * from.width) / 2, startY: fromY, endX: toX - (right * to.width) / 2, endY: toY, axis: \"x\" }\n  }\n  const down = toY >= fromY ? 1 : -1\n  return { startX: fromX, startY: fromY + (down * from.height) / 2, endX: toX, endY: toY - (down * to.height) / 2, axis: \"y\" }\n}\n\nconst n = (value: number) => Number(value.toFixed(2))\n\nfunction curvePath({ startX, startY, endX, endY }: BeamEnds, curvature: number) {\n  const dx = endX - startX\n  const dy = endY - startY\n  const length = Math.hypot(dx, dy) || 1\n  const controlX = (startX + endX) / 2 + (dy / length) * curvature\n  const controlY = (startY + endY) / 2 - (dx / length) * curvature\n  return `M ${n(startX)} ${n(startY)} Q ${n(controlX)} ${n(controlY)} ${n(endX)} ${n(endY)}`\n}\n\nfunction stepPath({ startX, startY, endX, endY, axis }: BeamEnds) {\n  const across = axis === \"x\" ? Math.abs(endY - startY) : Math.abs(endX - startX)\n  if (across < STRAIGHT_BELOW_PX) return `M ${n(startX)} ${n(startY)} L ${n(endX)} ${n(endY)}`\n  const along = axis === \"x\" ? Math.abs(endX - startX) : Math.abs(endY - startY)\n  const radius = Math.min(STEP_RADIUS, along / 2, across / 2)\n  if (axis === \"x\") {\n    const midX = (startX + endX) / 2\n    const dirX = endX >= startX ? 1 : -1\n    const dirY = endY >= startY ? 1 : -1\n    return [\n      `M ${n(startX)} ${n(startY)}`,\n      `L ${n(midX - dirX * radius)} ${n(startY)}`,\n      `Q ${n(midX)} ${n(startY)} ${n(midX)} ${n(startY + dirY * radius)}`,\n      `L ${n(midX)} ${n(endY - dirY * radius)}`,\n      `Q ${n(midX)} ${n(endY)} ${n(midX + dirX * radius)} ${n(endY)}`,\n      `L ${n(endX)} ${n(endY)}`,\n    ].join(\" \")\n  }\n  const midY = (startY + endY) / 2\n  const dirX = endX >= startX ? 1 : -1\n  const dirY = endY >= startY ? 1 : -1\n  return [\n    `M ${n(startX)} ${n(startY)}`,\n    `L ${n(startX)} ${n(midY - dirY * radius)}`,\n    `Q ${n(startX)} ${n(midY)} ${n(startX + dirX * radius)} ${n(midY)}`,\n    `L ${n(endX - dirX * radius)} ${n(midY)}`,\n    `Q ${n(endX)} ${n(midY)} ${n(endX)} ${n(midY + dirY * radius)}`,\n    `L ${n(endX)} ${n(endY)}`,\n  ].join(\" \")\n}\n\n/** Leaves and arrives square to the box, so many sources gather into one point without crossing it at odd angles. */\nfunction hubPath({ startX, startY, endX, endY, axis }: BeamEnds) {\n  if (axis === \"x\") {\n    const midX = (startX + endX) / 2\n    return `M ${n(startX)} ${n(startY)} C ${n(midX)} ${n(startY)} ${n(midX)} ${n(endY)} ${n(endX)} ${n(endY)}`\n  }\n  const midY = (startY + endY) / 2\n  return `M ${n(startX)} ${n(startY)} C ${n(startX)} ${n(midY)} ${n(endX)} ${n(midY)} ${n(endX)} ${n(endY)}`\n}\n\n/** `reverse` swaps the ends rather than the playback, so the light's bright head still leads the way it moves. */\nexport function beamPath(from: BeamBox, to: BeamBox, variant: BeamVariant, curvature = 40, reverse = false) {\n  if (!isDrawable(from, to)) return null\n  const ends = reverse ? beamEnds(to, from) : beamEnds(from, to)\n  if (variant === \"curve\") return curvePath(ends, reverse ? -curvature : curvature)\n  if (variant === \"step\") return stepPath(ends)\n  if (variant === \"hub\") return hubPath(ends)\n  return `M ${n(ends.startX)} ${n(ends.startY)} L ${n(ends.endX)} ${n(ends.endY)}`\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/background-loop.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useState, useSyncExternalStore } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\n/**\n * The accent as a light source, held in a lightness band per theme. A near-black accent would read as smoke on a light\n * page and a dark blue would pull muted text under 4.5:1 where a beam crosses it, so the band's floor is 0.6.\n * Older browsers keep plain --mise, which is only a stronger tint.\n */\nexport const LIGHT_TOKEN_CLASS = [\n  \"[--mise-light:var(--mise)]\",\n  \"supports-[color:oklch(from_red_l_c_h)]:[--mise-light:oklch(from_var(--mise)_clamp(0.6,l,0.76)_c_h)]\",\n  \"dark:supports-[color:oklch(from_red_l_c_h)]:[--mise-light:oklch(from_var(--mise)_clamp(0.62,l,0.82)_c_h)]\",\n].join(\" \")\n\n/** Before hydration the hook cannot know the preference yet. A paused animation holds its seeded frame. */\nexport const LOOP_CLASS = \"motion-reduce:[animation-play-state:paused]!\"\n\nconst subscribeToVisibility = (onChange: () => void) => {\n  document.addEventListener(\"visibilitychange\", onChange)\n  return () => document.removeEventListener(\"visibilitychange\", onChange)\n}\n\nfunction useTabVisible() {\n  return useSyncExternalStore(\n    subscribeToVisibility,\n    () => !document.hidden,\n    () => true,\n  )\n}\n\nfunction useOnScreen(rootRef: React.RefObject<HTMLElement | null>, watching: boolean) {\n  const [onScreen, setOnScreen] = useState(true)\n  useEffect(() => {\n    const root = rootRef.current\n    if (!watching || !root || typeof IntersectionObserver === \"undefined\") return\n    const view = new IntersectionObserver(([entry]) => setOnScreen(entry?.isIntersecting ?? true))\n    view.observe(root)\n    return () => view.disconnect()\n  }, [rootRef, watching])\n  return onScreen\n}\n\ninterface PlaybackOptions {\n  /** The owner's stop control. */\n  paused?: boolean\n  /** False when this look never moves, so nothing is watched at all. */\n  loops?: boolean\n}\n\n/** `still` draws one finished frame. `playing` is only true on screen, in a visible tab and not paused. */\nexport function useBackgroundPlayback(rootRef: React.RefObject<HTMLElement | null>, { paused = false, loops = true }: PlaybackOptions) {\n  const reduceMotion = useReducedMotion() ?? false\n  const moving = loops && !reduceMotion\n  const onScreen = useOnScreen(rootRef, moving)\n  const tabVisible = useTabVisible()\n  return { still: !moving, playing: moving && !paused && onScreen && tabVisible }\n}\n\n/** Longhands, because the shorthand would reset the play state each element reads from the layer. */\nexport function loopStyle(name: string, seconds: number, delay: number, timing = \"linear\", direction = \"normal\"): React.CSSProperties {\n  return {\n    animationName: name,\n    animationDuration: `${seconds}s`,\n    animationDelay: `${delay}s`,\n    animationTimingFunction: timing,\n    animationDirection: direction,\n    animationIterationCount: \"infinite\",\n    animationPlayState: \"var(--mise-bg-play)\",\n  }\n}\n\n/** Set once on the layer, so pausing writes one property however many elements are moving. */\nexport function playStyle(playing: boolean) {\n  return { \"--mise-bg-play\": playing ? \"running\" : \"paused\" } as React.CSSProperties\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "interactions"
  ],
  "type": "registry:ui"
}