{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "beams-background",
  "title": "Beams Background",
  "description": "Straight lines of light behind a section: a fan, lights on a grid, one slow band or soft shafts, quiet enough to keep the sentence on top first.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/beams-background.tsx",
      "content": "import type * as React from \"react\"\nimport { useMemo, useRef } from \"react\"\nimport { LIGHT_TOKEN_CLASS, LOOP_CLASS, loopStyle, playStyle, useBackgroundPlayback } from \"@/registry/miseui/ui/background-loop\"\nimport { columnLayout, rayGradient, rayLayout, travelLayout, type Travel } from \"@/registry/miseui/ui/beams-background-layout\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype BeamsBackgroundVariant = \"rays\" | \"lines\" | \"sweep\" | \"columns\"\ntype BeamsBackgroundEffect = \"flow\" | \"still\"\n\nconst GRID_PX = 48\nconst TAIL_PX = 96\nconst SWEEP_OPACITY = 0.18\n\nconst KEYFRAMES = [\n  \"@keyframes mise-beam-breathe{0%,100%{opacity:calc(var(--mise-o)*.45)}50%{opacity:var(--mise-o)}}\",\n  `@keyframes mise-beam-x{0%{transform:translateX(-100%);opacity:0}8%,42%{opacity:var(--mise-o)}50%,100%{transform:translateX(calc(100% + ${TAIL_PX}px));opacity:0}}`,\n  `@keyframes mise-beam-y{0%{transform:translateY(-100%);opacity:0}8%,42%{opacity:var(--mise-o)}50%,100%{transform:translateY(calc(100% + ${TAIL_PX}px));opacity:0}}`,\n  \"@keyframes mise-beam-sweep{from{transform:translateX(-80%) skewX(-16deg)}to{transform:translateX(120%) skewX(-16deg)}}\",\n  \"@keyframes mise-beam-shaft{0%,100%{transform:translateY(var(--mise-from));opacity:calc(var(--mise-o)*.5)}50%{transform:translateY(var(--mise-to));opacity:var(--mise-o)}}\",\n].join(\"\")\n\nconst RAY_MASK = \"radial-gradient(ellipse 56% 94% at 50% 0%, #000 10%, transparent 100%)\"\nconst SHAFT_MASK = \"linear-gradient(90deg, transparent, #000 50%, transparent)\"\nconst GRID_STYLE: React.CSSProperties = {\n  backgroundImage: \"linear-gradient(to right, var(--border) 1px, transparent 1px), linear-gradient(to bottom, var(--border) 1px, transparent 1px)\",\n  backgroundSize: `${GRID_PX}px ${GRID_PX}px`,\n  maskImage: \"radial-gradient(ellipse closest-side, #000 45%, transparent 100%)\",\n  // The lights are the subject and a grid line can cross the text, so the grid itself stays barely there.\n  opacity: 0.55,\n}\n\n/** Percent would drift off the hairlines as the box resizes; round() snaps it back on to the 48 px grid. */\nconst AT_CLASS = {\n  x: \"inset-x-0 h-px top-(--mise-at) supports-[top:round(down,1%,1px)]:top-[round(down,var(--mise-at),48px)]\",\n  y: \"inset-y-0 w-px left-(--mise-at) supports-[left:round(down,1%,1px)]:left-[round(down,var(--mise-at),48px)]\",\n} as const\n\ninterface FaceProps {\n  seed: number\n  still: boolean\n}\n\nfunction vars(values: Record<string, string | number>) {\n  return values as React.CSSProperties\n}\n\nfunction RaysFace({ seed, still }: FaceProps) {\n  const rays = useMemo(() => rayLayout(seed), [seed])\n  return rays.map((ray) => (\n    <span\n      key={ray.angle}\n      data-slot=\"beams-background-ray\"\n      className={cn(\"absolute inset-0\", LOOP_CLASS)}\n      style={{\n        backgroundImage: rayGradient(ray),\n        maskImage: RAY_MASK,\n        ...vars({ \"--mise-o\": ray.peak }),\n        ...(still ? { opacity: ray.peak * 0.8 } : loopStyle(\"mise-beam-breathe\", ray.seconds, ray.delay, \"ease-in-out\")),\n      }}\n    />\n  ))\n}\n\nfunction Light({ travel, still }: { travel: Travel; still: boolean }) {\n  const horizontal = travel.axis === \"x\"\n  return (\n    <span\n      data-slot=\"beams-background-light\"\n      data-axis={travel.axis}\n      className={cn(\"absolute\", AT_CLASS[travel.axis], LOOP_CLASS)}\n      style={{\n        ...vars({ \"--mise-at\": `${travel.at}%`, \"--mise-o\": travel.peak }),\n        ...(still\n          ? { opacity: travel.peak, transform: `translate${horizontal ? \"X\" : \"Y\"}(${travel.still}%)` }\n          : loopStyle(`mise-beam-${travel.axis}`, travel.seconds, travel.delay)),\n      }}\n    >\n      <span\n        className={cn(\"absolute\", horizontal ? \"right-0 h-full w-24 bg-linear-to-r\" : \"bottom-0 h-24 w-full bg-linear-to-b\", \"from-transparent to-(--mise-light)\")}\n      />\n    </span>\n  )\n}\n\nfunction LinesFace({ seed, still }: FaceProps) {\n  const travels = useMemo(() => travelLayout(seed), [seed])\n  return (\n    <>\n      <span data-slot=\"beams-background-grid\" className=\"absolute inset-0\" style={GRID_STYLE} />\n      {travels.map((travel) => (\n        <Light key={`${travel.axis}${travel.at}`} travel={travel} still={still} />\n      ))}\n    </>\n  )\n}\n\nfunction SweepFace({ still }: FaceProps) {\n  return (\n    <span\n      data-slot=\"beams-background-band\"\n      className={cn(\"absolute inset-y-[-20%] left-0 w-[70%] bg-linear-to-r from-transparent via-(--mise-light) to-transparent\", LOOP_CLASS)}\n      style={{\n        opacity: SWEEP_OPACITY,\n        ...(still ? { transform: \"translateX(45%) skewX(-16deg)\" } : loopStyle(\"mise-beam-sweep\", 16, -5, \"ease-in-out\", \"alternate\")),\n      }}\n    />\n  )\n}\n\nfunction ColumnsFace({ seed, still }: FaceProps) {\n  const columns = useMemo(() => columnLayout(seed), [seed])\n  return columns.map((column) => (\n    <span\n      key={column.left}\n      data-slot=\"beams-background-column\"\n      className={cn(\"absolute bg-linear-to-b from-transparent via-(--mise-light) to-transparent\", LOOP_CLASS)}\n      style={{\n        left: `${column.left}%`,\n        top: `${column.top}%`,\n        width: `${column.width}%`,\n        height: `${column.height}%`,\n        maskImage: SHAFT_MASK,\n        ...vars({ \"--mise-o\": column.peak, \"--mise-from\": `${column.from}%`, \"--mise-to\": `${column.to}%` }),\n        ...(still ? { opacity: column.peak * 0.8 } : loopStyle(\"mise-beam-shaft\", column.seconds, column.delay, \"ease-in-out\")),\n      }}\n    />\n  ))\n}\n\nconst FACE = { rays: RaysFace, lines: LinesFace, sweep: SweepFace, columns: ColumnsFace } as const\n\ntype BeamsBackgroundProps = Omit<React.ComponentProps<\"div\">, \"ref\"> & {\n  /** `rays` a fan from above, `lines` lights on a faint grid, `sweep` one wide band crossing, `columns` soft vertical shafts. */\n  variant?: BeamsBackgroundVariant\n  /** `flow` moves while on screen. `still` draws one finished frame, the same one reduced motion gets. */\n  effect?: BeamsBackgroundEffect\n  /** Holds the motion. Wire it to a control: motion past five seconds needs a way to stop it. */\n  paused?: boolean\n  /** Another seed gives another layout that is still the same on the server and the client. */\n  seed?: number\n}\n\nfunction BeamsBackground({ variant = \"rays\", effect = \"flow\", paused = false, seed = 1, className, children, ...props }: BeamsBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const { still, playing } = useBackgroundPlayback(rootRef, { paused, loops: effect === \"flow\" })\n  const Face = FACE[variant]\n  return (\n    <div\n      {...props}\n      ref={rootRef}\n      data-slot=\"beams-background\"\n      data-variant={variant}\n      data-effect={effect}\n      data-playing={still ? undefined : String(playing)}\n      className={cn(\"relative isolate min-w-0 overflow-clip\", className)}\n    >\n      {still ? null : <style href=\"mise-beams-background\" precedence=\"default\">{KEYFRAMES}</style>}\n      <div\n        aria-hidden=\"true\"\n        data-slot=\"beams-background-layer\"\n        className={cn(\"pointer-events-none absolute inset-0 -z-10 overflow-clip rounded-[inherit]\", LIGHT_TOKEN_CLASS)}\n        style={playStyle(playing)}\n      >\n        <Face seed={seed} still={still} />\n      </div>\n      <div data-slot=\"beams-background-content\" className=\"relative h-full\">\n        {children}\n      </div>\n    </div>\n  )\n}\n\nexport { BeamsBackground, type BeamsBackgroundProps, type BeamsBackgroundVariant, type BeamsBackgroundEffect }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/beams-background-layout.ts",
      "content": "import { between, seededRandom } from \"@/registry/miseui/ui/background-seed\"\n\n/** A beam this strong over the page keeps muted text at 4.5:1 in all five palettes, in both themes. */\nexport const MAX_WASH_OPACITY = 0.18\n\nconst RAY_COUNT = 7\nconst RAY_FAN_DEGREES = 88\nconst COLUMN_COUNT = 13\n\nexport interface Ray {\n  /** Degrees from straight down. */\n  angle: number\n  spread: number\n  peak: number\n  seconds: number\n  delay: number\n}\n\nexport interface Travel {\n  axis: \"x\" | \"y\"\n  /** Percent of the box. CSS rounds it down onto the grid. */\n  at: number\n  seconds: number\n  delay: number\n  peak: number\n  /** Translate percent for the frozen frame, chosen so the light is inside the box. */\n  still: number\n}\n\nexport interface Column {\n  left: number\n  top: number\n  width: number\n  height: number\n  from: number\n  to: number\n  seconds: number\n  delay: number\n  peak: number\n}\n\nexport function rayLayout(seed: number): readonly Ray[] {\n  const random = seededRandom(seed)\n  return Array.from({ length: RAY_COUNT }, (_, index) => {\n    const seconds = between(random, 8, 14)\n    return {\n      angle: between(random, -3, 3) + (index / (RAY_COUNT - 1) - 0.5) * RAY_FAN_DEGREES,\n      spread: between(random, 2.5, 5.5),\n      peak: between(random, 0.09, 0.18),\n      seconds,\n      delay: -between(random, 0, seconds),\n    }\n  })\n}\n\n/** One soft wedge. Conic, so wedges never overlap into a hot core where a stack of bars would. */\nexport function rayGradient({ angle, spread }: Ray) {\n  const start = Math.round((180 + angle - spread) * 100) / 100\n  return `conic-gradient(from ${start}deg at 50% -12%, transparent 0deg, var(--mise-light) ${spread}deg, transparent ${spread * 2}deg)`\n}\n\nfunction travelsOn(axis: Travel[\"axis\"], slots: readonly number[], random: () => number): readonly Travel[] {\n  return slots.map((slot) => {\n    const seconds = between(random, 11, 18)\n    return {\n      axis,\n      at: slot + between(random, -6, 6),\n      seconds,\n      delay: -between(random, 0, seconds),\n      peak: between(random, 0.55, 0.8),\n      still: -between(random, 15, 70),\n    }\n  })\n}\n\nexport function travelLayout(seed: number): readonly Travel[] {\n  const random = seededRandom(seed)\n  return [...travelsOn(\"x\", [24, 52, 80], random), ...travelsOn(\"y\", [18, 50, 82], random)]\n}\n\nexport function columnLayout(seed: number): readonly Column[] {\n  const random = seededRandom(seed)\n  return Array.from({ length: COLUMN_COUNT }, (_, index) => {\n    const height = between(random, 38, 82)\n    const seconds = between(random, 10, 18)\n    const reach = between(random, 3, 7)\n    return {\n      left: 3 + ((index + 0.5) / COLUMN_COUNT) * 94 + between(random, -1.5, 1.5),\n      top: between(random, 4, 96 - height),\n      width: between(random, 2.5, 6),\n      height,\n      from: -reach,\n      to: reach,\n      seconds,\n      delay: -between(random, 0, seconds),\n      peak: between(random, 0.07, 0.18),\n    }\n  })\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"
    },
    {
      "path": "registry/miseui/ui/background-seed.ts",
      "content": "/** Mulberry32. Server and client walk the same numbers, so a seeded layout hydrates without a mismatch. */\nexport function seededRandom(seed: number) {\n  let state = seed >>> 0\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0\n    let mixed = state\n    mixed = Math.imul(mixed ^ (mixed >>> 15), mixed | 1)\n    mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61)\n    return ((mixed ^ (mixed >>> 14)) >>> 0) / 4294967296\n  }\n}\n\n/** Two decimals: enough for a percent or a second, and short in the inline style. */\nexport function between(random: () => number, min: number, max: number) {\n  return Math.round((min + random() * (max - min)) * 100) / 100\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "type": "registry:ui"
}