{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"glow","type":"registry:component","title":"Glow","description":"Ambient light around one element that says something is happening: an aurora under a working composer, or a ring of light round a live card.","categories":["effects"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/glow/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/glow/defaults.ts","content":"/** Pure allowed presentation defaults for Glow; no component or runtime imports. */\nexport interface GlowDefaults {\n  /** Shared effect setting; omit to inherit the component default. Scoped defaults retain the legacy key variant. */\n  variant?: \"aurora\" | \"ring\";\n}\n"},{"path":"beamline/components/glow/glow.module.css","type":"registry:file","target":"src/ui/beamline/components/glow/glow.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Light that says \"working\". Aurora: colour pools under and behind the element and drifts. Ring: a band of colour\n   travels around the edge. Both are blurred layers that only translate or rotate (compositor work), fade in and out\n   with `active`, stop entirely when inactive, and hold still off screen. Colours come from --pui-glow-1/2/3; every\n   period is a multiple of --pui-duration-considered, so the scope's animation speed reaches the light too. */\n  .root {\n    --pui-r: var(--pui-radius-card);\n    --pui-i: 1;\n    position: relative;\n    isolation: isolate;\n    border-radius: var(--pui-r);\n  }\n  .root > .field {\n    position: absolute;\n    z-index: -1;\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity calc(var(--pui-duration-considered) * 2) var(--pui-ease-standard);\n  }\n  .root[data-active] > .field {\n    opacity: calc(0.9 * var(--pui-i));\n  }\n\n  /* aurora: three soft blobs along the lower edge, each drifting on its own period */\n  .aurora {\n    inset: 30% -6% -34%;\n    filter: blur(22px);\n  }\n  .blob {\n    position: absolute;\n    bottom: 0;\n    width: 46%;\n    height: 70%;\n    border-radius: 50%;\n    will-change: transform;\n  }\n  .blob:nth-child(1) {\n    left: 2%;\n    background: var(--pui-glow-1);\n    animation: glow-drift-a calc(var(--pui-duration-considered) * 19.5) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(2) {\n    left: 30%;\n    background: var(--pui-glow-2);\n    animation: glow-drift-b calc(var(--pui-duration-considered) * 25) var(--pui-ease-in-out) infinite alternate;\n  }\n  .blob:nth-child(3) {\n    right: 2%;\n    background: var(--pui-glow-3);\n    animation: glow-drift-c calc(var(--pui-duration-considered) * 22) var(--pui-ease-in-out) infinite alternate;\n  }\n\n  /* ring: a conic band (a square as wide as the element's diagonal, set from its size) rotating inside a mask that leaves only a thin frame plus its soft bloom */\n  .ring {\n    inset: -1px;\n    border-radius: calc(var(--pui-r) + 1px);\n    padding: 1.5px;\n    overflow: hidden;\n    -webkit-mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n    mask:\n      linear-gradient(#000 0 0) content-box exclude,\n      linear-gradient(#000 0 0);\n  }\n  .bloom {\n    inset: -10px;\n    border-radius: calc(var(--pui-r) + 10px);\n    overflow: hidden;\n    filter: blur(12px);\n  }\n  .band {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    width: var(--pui-d, 150%);\n    aspect-ratio: 1;\n    translate: -50% -50%;\n    will-change: transform;\n    background: conic-gradient(\n      from 0deg,\n      transparent 0 58%,\n      var(--pui-glow-1) 70%,\n      var(--pui-glow-2) 80%,\n      var(--pui-glow-3) 88%,\n      transparent 96%\n    );\n    animation: glow-turn calc(var(--pui-duration-considered) * 10) linear infinite;\n  }\n  .bloom .band {\n    opacity: 0.55;\n  }\n\n  .root:not([data-active]) .blob,\n  .root:not([data-active]) .band {\n    animation-play-state: paused;\n  }\n  .root[data-paused] .blob,\n  .root[data-paused] .band {\n    animation-play-state: paused;\n  }\n  .root[data-reduced] .blob,\n  .root[data-reduced] .band {\n    animation: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .blob,\n    .band {\n      animation: none !important;\n    }\n  }\n}\n@keyframes glow-drift-a {\n  to {\n    transform: translate(18%, -12%) scale(1.15);\n  }\n}\n@keyframes glow-drift-b {\n  to {\n    transform: translate(-14%, 8%) scale(0.9);\n  }\n}\n@keyframes glow-drift-c {\n  to {\n    transform: translate(-20%, -10%) scale(1.1);\n  }\n}\n@keyframes glow-turn {\n  to {\n    transform: rotate(1turn);\n  }\n}\n"},{"path":"beamline/components/glow/glow.tsx","type":"registry:file","target":"src/ui/beamline/components/glow/glow.tsx","content":"\"use client\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { GlowDefaults } from \"./defaults\";\nexport type { GlowDefaults } from \"./defaults\";\n\nimport { useEffect, useRef, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./glow.module.css\";\n\nexport interface GlowClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the field part; no override by default. */\n  field?: string;\n  /** Classes for the blob part; no override by default. */\n  blob?: string;\n  /** Classes for the bloom part; no override by default. */\n  bloom?: string;\n  /** Classes for the ring part; no override by default. */\n  ring?: string;\n  /** Classes for the band part; no override by default. */\n  band?: string;\n}\n\nexport interface GlowProps {\n  /** Content rendered inside the component. Defaults to no children. */\n  children?: ReactNode;\n  /** While true the glow fades in and moves; false fades it out and stops all motion. */\n  active?: boolean;\n  /** Aurora light behind the content or a ring around it; defaults to aurora. */\n  effect?: \"aurora\" | \"ring\";\n  /** Legacy effect setting; omitted by default. @deprecated Use effect instead. */\n  variant?: \"aurora\" | \"ring\";\n  /** 0–1, how strong the light is. */\n  intensity?: number;\n  /** Corner radius of the wrapped element, so the ring follows it. Defaults to the card radius. */\n  radius?: string;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: GlowClassNames;\n}\n\n/**\n * Ambient light around one element that says \"something is happening here\": the composer while an agent works, the\n * live card, the call in progress. One running per screen (bind `active` to a state that ends). Compositor-only motion; nothing runs while inactive or off screen.\n */\nexport function Glow({\n  children,\n  active = true,\n  effect: effectProp,\n  variant: variantProp,\n  intensity = 1,\n  radius,\n  className,\n  classNames,\n  style,\n  ref,\n}: GlowProps) {\n  const scopeDefaults = useComponentDefaults<GlowDefaults>(\"glow\", [\"variant\"]);\n  useDeprecatedProp(\n    \"glow\",\n    \"variant\",\n    \"effect\",\n    variantProp !== undefined || (effectProp === undefined && scopeDefaults.variant !== undefined),\n  );\n  const variant = effectProp ?? variantProp ?? scopeDefaults.variant ?? \"aurora\";\n\n  const own = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(own, ref);\n  const tokens = useMotionTokens(own);\n  const managedReduced = useReducedMotionSafe();\n  const onScreen = useActive(own);\n  // The ring's rotating band must cover the element's diagonal and no more (a larger layer only costs GPU memory).\n  useEffect(() => {\n    const el = own.current;\n    if (!el || variant !== \"ring\" || typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => {\n      const { width, height } = e.contentRect;\n      el.style.setProperty(\"--pui-d\", `${Math.ceil(Math.hypot(width, height) + 24)}px`);\n    });\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [variant]);\n  return (\n    <div\n      ref={attach}\n      data-slot=\"glow\"\n      data-variant={variant}\n      data-active={active || undefined}\n      data-paused={onScreen ? undefined : \"\"}\n      data-reduced={tokens.reduced || managedReduced ? \"\" : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n      style={{ ...(radius ? { \"--pui-r\": radius } : {}), \"--pui-i\": intensity, ...style } as CSSProperties}\n    >\n      {variant === \"aurora\" ? (\n        <span data-slot=\"glow-field\" aria-hidden=\"true\" className={cn(styles.field, styles.aurora, classNames?.field)}>\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n          <span data-slot=\"glow-blob\" className={cn(styles.blob, classNames?.blob)} />\n        </span>\n      ) : (\n        <>\n          <span\n            data-slot=\"glow-bloom\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.bloom, classNames?.field, classNames?.bloom)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n          <span\n            data-slot=\"glow-ring\"\n            aria-hidden=\"true\"\n            className={cn(styles.field, styles.ring, classNames?.field, classNames?.ring)}\n          >\n            <span data-slot=\"glow-band\" className={cn(styles.band, classNames?.band)} />\n          </span>\n        </>\n      )}\n      {children}\n    </div>\n  );\n}\n\nexport default Glow;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-media-query.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-media-query.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// one MediaQueryList per query: React reads the snapshot on every render, and a part that renders thirty times a\n// second (a playing audio player's clock) would otherwise ask the browser for a new list each time\nconst lists = new Map<string, MediaQueryList>();\nconst list = (query: string) => {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return undefined;\n  let m = lists.get(query);\n  if (!m) lists.set(query, (m = window.matchMedia(query)));\n  return m;\n};\n\n/** True while the media query matches; false on the server and in the first client render of SSR output. */\nexport function useMediaQuery(query: string): boolean {\n  return useSyncExternalStore(\n    (notify) => {\n      const m = list(query);\n      m?.addEventListener(\"change\", notify);\n      return () => m?.removeEventListener(\"change\", notify);\n    },\n    () => list(query)?.matches ?? false,\n    () => false,\n  );\n}\n\n/** Phone-width layouts: below 768px. */\nexport const usePhone = () => useMediaQuery(\"(max-width: 767px)\");\n"},{"path":"beamline/hooks/use-motion-tokens.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-motion-tokens.ts","content":"\"use client\";\n\nimport { useMemo, type RefObject } from \"react\";\nimport { useAppearanceRoot } from \"../runtime/appearance\";\nimport { useResolvedAppearance } from \"./use-resolved-appearance\";\nimport { motionAt, motionEase, motionTokens, type MotionTokens } from \"../lib/motion-tokens\";\nimport { semanticMotion } from \"../lib/motion-source\";\n\nconst durationToken = (name: string) =>\n  name === \"exit\" ? \"motion-out\" : name === \"in\" || name === \"out\" ? `motion-${name}` : `duration-${name}`;\nconst easeToken = (name: string) => `ease-${name === \"inOut\" ? \"in-out\" : name}`;\nconst REQUEST = {\n  times: [\n    ...Object.keys(semanticMotion.duration).map(durationToken),\n    ...Object.keys(semanticMotion.spring).map((name) => `spring-${name}`),\n    ...Object.keys(semanticMotion.stagger).map((name) => `stagger-${name}`),\n    \"fade-in\",\n    \"fade-out\",\n  ],\n  easings: [...Object.keys(semanticMotion.ease).map(easeToken), \"ease-spring\", \"ease-spring-bounce\", \"ease-overshoot\"],\n  numbers: [\"motion-scale\", \"motion-look\", \"press-scale\"],\n  // timing only: the host's own type would make every control resolve differently from the page around it\n  typography: false,\n};\n\n/**\n * Resolve each timing at the actual host; omitted host uses the nearest managed root. The first/server snapshot is\n * the pure foundation default. Local CSS values, scope changes, font readiness, OS preference and explicit refresh\n * update the same consumer; no document-wide first-root fallback or persistent global listener.\n */\nexport function useMotionTokens(hostRef?: RefObject<HTMLElement | null>): MotionTokens & {\n  ready: boolean;\n  /** Reduced or none: movement, springs and staggers are instant. */\n  reduced: boolean;\n  mode: \"full\" | \"reduced\" | \"none\";\n  /** Opacity-only arrival and departure (seconds): kept under reduced, instant under none (APPEARANCE A06). */\n  fade: { in: number; out: number };\n} {\n  const root = useAppearanceRoot();\n  const scopeRef = useMemo(() => ({ current: root }), [root]);\n  const resolved = useResolvedAppearance(hostRef ?? scopeRef, REQUEST);\n  return useMemo(() => {\n    if (!resolved.ready)\n      return {\n        ...motionTokens,\n        ready: false,\n        reduced: false,\n        mode: \"full\" as const,\n        fade: { in: motionTokens.duration.in, out: motionTokens.duration.out },\n      };\n    const reduced = resolved.reducedMotion;\n    const speed = (resolved.numbers[\"motion-scale\"] ?? 1) * (resolved.numbers[\"motion-look\"] ?? 1);\n    const fade =\n      resolved.motionMode === \"none\"\n        ? { in: 0.001, out: 0.001 }\n        : {\n            in: resolved.times[\"fade-in\"] ?? semanticMotion.duration.in * speed,\n            out: resolved.times[\"fade-out\"] ?? semanticMotion.duration.out * speed,\n          };\n    const factor = reduced ? 0.001 / semanticMotion.duration.standard : speed;\n    const base = motionAt(factor);\n    const duration = Object.fromEntries(\n      Object.entries(base.duration).map(([name, fallback]) => [\n        name,\n        reduced ? 0.001 : (resolved.times[durationToken(name)] ?? fallback),\n      ]),\n    ) as MotionTokens[\"duration\"];\n    const stagger = Object.fromEntries(\n      Object.entries(base.stagger).map(([name, fallback]) => [\n        name,\n        reduced ? 0 : (resolved.times[`stagger-${name}`] ?? fallback),\n      ]),\n    ) as MotionTokens[\"stagger\"];\n    const ease = Object.fromEntries(\n      Object.entries(base.ease).map(([name, fallback]) => [\n        name,\n        motionEase(resolved.easings[easeToken(name)], fallback),\n      ]),\n    ) as MotionTokens[\"ease\"];\n    const spring = { ...base.spring };\n    for (const name of Object.keys(semanticMotion.spring) as (keyof typeof semanticMotion.spring)[])\n      spring[name] = {\n        ...base.spring[name],\n        visualDuration: reduced ? 0.001 : (resolved.times[`spring-${name}`] ?? base.spring[name].visualDuration),\n        bounce: reduced ? 0 : base.spring[name].bounce,\n      };\n    return {\n      ...base,\n      ready: true,\n      reduced,\n      mode: resolved.motionMode,\n      fade,\n      duration,\n      stagger,\n      ease,\n      spring,\n      pressScale: resolved.numbers[\"press-scale\"] ?? base.pressScale,\n      css: {\n        spring: resolved.easings[\"ease-spring\"] ?? base.css.spring,\n        springBounce: resolved.easings[\"ease-spring-bounce\"] ?? base.css.springBounce,\n        overshoot: resolved.easings[\"ease-overshoot\"] ?? base.css.overshoot,\n      },\n    };\n  }, [resolved]);\n}\n"},{"path":"beamline/hooks/use-reduced-motion-safe.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-reduced-motion-safe.ts","content":"\"use client\";\n\nimport { useMediaQuery } from \"./use-media-query\";\nimport { useScopeMotion } from \"../runtime/appearance\";\n\n/** Managed motion policy plus the OS preference, with a stable server snapshot. */\nexport function useReducedMotionSafe() {\n  const requested = useScopeMotion();\n  const os = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return requested !== \"auto\" || os;\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Glow is in src/ui/beamline/components/glow/glow.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/glow"}}