{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-carousel","type":"registry:component","title":"Logo Carousel","description":"A wall of logos shown a few at a time: every few seconds the slots turn left to right, each logo leaving upward as the next rises in.","categories":["display"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","motion@^13.4.4","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/logo-carousel/logo-carousel.module.css","type":"registry:file","target":"src/ui/beamline/components/logo-carousel/logo-carousel.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  .root {\n    display: grid;\n    align-items: center;\n    gap: var(--pui-space-3) var(--pui-space-8);\n    width: 100%;\n  }\n  .slot {\n    position: relative;\n    display: grid;\n    place-items: center;\n    height: 40px;\n    overflow: hidden;\n    /* a logo leaving or arriving dissolves at the slot's edges instead of being cut by them */\n    mask-image: linear-gradient(transparent, black 25%, black 75%, transparent);\n  }\n  .logo {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-1-5);\n    color: var(--pui-ink-2);\n    white-space: nowrap;\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  /* the wall holds while pointed at, and the logo under the pointer comes up to full ink at once */\n  .slot:hover .logo {\n    color: var(--pui-ink);\n    transition-duration: 0ms;\n  }\n  .logo :is(svg) {\n    flex: none;\n  }\n}\n"},{"path":"beamline/components/logo-carousel/logo-carousel.tsx","type":"registry:file","target":"src/ui/beamline/components/logo-carousel/logo-carousel.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useEffect, useLayoutEffect, useRef, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { AnimatePresence, motion, type Transition } from \"motion/react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useHold } from \"../../hooks/use-hold\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./logo-carousel.module.css\";\n\nexport interface CarouselLogo {\n  /** Company or product name: the accessible name and the key. */\n  name: string;\n  logo: ReactNode;\n}\n\nexport interface LogoCarouselClassNames {\n  root?: string;\n  slot?: string;\n  logo?: string;\n}\n\nexport interface LogoCarouselProps extends Omit<ComponentProps<\"div\">, \"children\"> {\n  logos: CarouselLogo[];\n  classNames?: LogoCarouselClassNames;\n  /** How many logos show at once. \"fit\": as many whole logos as the room holds side by side; when every logo fits,\n   *  the wall stands still (no turning, nothing running). */\n  columns?: number | \"fit\";\n  /** Milliseconds between turns. */\n  interval?: number;\n  /** Accessible name of the wall, e.g. \"Teams on Northline\". */\n  label?: string;\n}\n\n/**\n * A wall of logos shown a few at a time (or, with columns \"fit\", as many as its room holds). Every `interval` the slots turn one after another, left to right: the logo\n * leaves upward and blurs out as the next one rises in. Holds still while hovered or focused (a turning wall is a tab\n * stop), after a tap on touch until the next tap, off screen, or in a hidden tab.\n */\nexport function LogoCarousel({\n  logos,\n  columns = 4,\n  interval = 2600,\n  label: labelProp,\n  className,\n  classNames,\n  style,\n  ref,\n  ...props\n}: LogoCarouselProps) {\n  const i18n = useMessages(\"logo-carousel\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const active = useActive(host);\n  const requestedReduced = useReducedMotionSafe();\n  const reduce = requestedReduced || motionTokens.reduced;\n  const [turn, setTurn] = useState(0);\n  // the shared hold (WCAG 2.2.2, pause what moves): the pointer over it, keyboard focus, or a tap on touch until the next\n  const { held, handlers: holdHandlers } = useHold(host);\n  const fitting = columns === \"fit\";\n  // \"fit\": how many whole logos the room holds, measured below; the widest logo seen so far only grows, so the count\n  // settles instead of trading places as narrower and wider logos come and go\n  const [fitCols, setFitCols] = useState(() => Math.min(4, Math.max(logos.length, 1)));\n  const widestSeen = useRef(0);\n  const logoSet = logos.map((l) => l.name).join(\"\\u0001\");\n  const measuredSet = useRef(logoSet);\n  const cols = Math.max(1, Math.min(fitting ? fitCols : columns, logos.length));\n  const pages = Math.max(1, Math.ceil(logos.length / cols));\n\n  // logos per row: all `cols` show at once, in two or more even rows when whole logos do not fit side by side (a\n  // phone); a slot never cuts its logo. A logo keeps its own width whatever its slot's, so one measurement before\n  // paint (and again when the wall's size or its logos change) gives the count; the same count renders nothing again.\n  const [perRow, setPerRow] = useState(cols);\n  useLayoutEffect(() => {\n    const el = host.current;\n    if (!el) return;\n    const fit = () => {\n      const s = getComputedStyle(el);\n      const gap = parseFloat(s.columnGap) || 0;\n      const width = el.clientWidth - parseFloat(s.paddingLeft) - parseFloat(s.paddingRight);\n      const shown = el.querySelectorAll<HTMLElement>('[data-slot=\"logo-carousel-logo\"]');\n      const widest = Math.max(0, ...[...shown].map((l) => l.offsetWidth));\n      if (fitting) {\n        // another set of logos starts its measure afresh\n        if (measuredSet.current !== logoSet) {\n          measuredSet.current = logoSet;\n          widestSeen.current = 0;\n        }\n        widestSeen.current = Math.max(widestSeen.current, widest);\n        const n = Math.max(1, Math.min(logos.length, Math.floor((width + gap) / (widestSeen.current + gap))));\n        setFitCols(n);\n        setPerRow(n);\n        return;\n      }\n      let n = cols;\n      while (n > 1 && (width - (n - 1) * gap) / n < widest) n--;\n      setPerRow(Math.ceil(cols / Math.ceil(cols / n))); // 2 + 2, not 3 + 1\n    };\n    fit();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(fit);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [cols, turn, fitting, logos.length, logoSet]);\n\n  // a turning logo wall is a decorative loop: reduced motion holds the first set still (DESIGN-LANGUAGE motion)\n  useEffect(() => {\n    if (!active || held || reduce || pages < 2) return;\n    const id = window.setInterval(() => setTurn((t) => t + 1), interval);\n    return () => window.clearInterval(id);\n  }, [active, held, reduce, pages, interval]);\n  // while it turns the wall is one tab stop, so a keyboard can hold it still the way a pointer does (like Matrix's\n  // scrolling text); a still wall is a picture with no stop\n  const turning = pages >= 2 && !reduce;\n\n  return (\n    <div\n      ref={attach}\n      role=\"list\"\n      aria-label={label}\n      data-slot=\"logo-carousel\"\n      data-held={held || undefined}\n      tabIndex={turning ? 0 : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n      style={{ gridTemplateColumns: `repeat(${perRow}, minmax(0, 1fr))`, ...style }}\n      {...holdHandlers}\n      {...props}\n    >\n      {Array.from({ length: cols }, (_, col) => {\n        const i = ((turn % pages) * cols + col) % logos.length;\n        const item = logos[i];\n        const t: Transition = reduce\n          ? { duration: motionTokens.duration.standard }\n          : {\n              duration: motionTokens.duration.considered,\n              ease: motionTokens.ease.standard,\n              delay: col * motionTokens.stagger.line,\n            };\n        return (\n          <div key={col} role=\"listitem\" className={cn(styles.slot, classNames?.slot)} data-slot=\"logo-carousel-slot\">\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              <motion.div\n                key={item.name}\n                aria-label={item.name}\n                role=\"img\"\n                data-slot=\"logo-carousel-logo\"\n                className={cn(styles.logo, classNames?.logo)}\n                // a whole transform (not `y`) so the browser runs the rise on the compositor, like the fade and blur:\n                // a turning wall asks nothing of the page's main thread\n                initial={\n                  reduce\n                    ? { opacity: 0 }\n                    : { opacity: 0, transform: \"translateY(70%)\", filter: `blur(${motionTokens.blur.soft}px)` }\n                }\n                animate={{ opacity: 1, transform: \"translateY(0%)\", filter: \"blur(0px)\" }}\n                exit={\n                  reduce\n                    ? { opacity: 0 }\n                    : { opacity: 0, transform: \"translateY(-70%)\", filter: `blur(${motionTokens.blur.soft}px)` }\n                }\n                transition={t}\n              >\n                {item.logo}\n              </motion.div>\n            </AnimatePresence>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport default LogoCarousel;\n"},{"path":"beamline/components/logo-carousel/messages.ts","type":"registry:file","target":"src/ui/beamline/components/logo-carousel/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  label: \"Logos\",\n} satisfies MessageDefinitions;\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-hold.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-hold.ts","content":"\"use client\";\n\nimport { useState, type FocusEvent, type PointerEvent, type RefObject } from \"react\";\nimport { useTap, useTouchAway } from \"./use-tap\";\n\n/**\n * Holds still what moves on its own (WCAG 2.2.2, pause what moves for more than five seconds), the same way for\n * everyone: a mouse over it, keyboard focus inside it (a moving element with nothing focusable inside is a tab stop\n * while it moves, so a keyboard can reach it), or on touch, where there is no hover, a finger on it and then a\n * tap, which holds it until the next tap on it. A tap that chooses something inside it (a button, a link) holds it\n * the way focus does: until the next touch somewhere else, so what was chosen does not run away. A touch that scrolls\n * the page is not a tap (the shared `useTap`). Spread `handlers` on the moving element's root (call them from your own when the root has\n * its own pointer handlers) and stop the motion while `held`.\n */\nexport function useHold(ref: RefObject<HTMLElement | null>) {\n  const [over, setOver] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const [pressing, setPressing] = useState(false);\n  // a touch hold: \"tap\" lasts until the next tap on it, \"choice\" until a touch elsewhere\n  const [pin, setPin] = useState<\"tap\" | \"choice\" | null>(null);\n  useTouchAway(ref, pin === \"choice\", () => setPin(null));\n  const tap = useTap((e) => {\n    const root = e.currentTarget;\n    const hit = (e.target as Element).closest?.(\"button, a[href], input, select, textarea, [role=button]\");\n    const chose = !!hit && hit !== root && root.contains(hit);\n    setPin((p) => (chose ? \"choice\" : p ? null : \"tap\"));\n  });\n\n  const handlers = {\n    onPointerEnter: (e: PointerEvent<HTMLElement>) => {\n      if (e.pointerType === \"mouse\") setOver(true);\n    },\n    onPointerLeave: (e: PointerEvent<HTMLElement>) => {\n      if (e.pointerType === \"mouse\") setOver(false);\n    },\n    // a finger on it holds it while it is down\n    onPointerDown: (e: PointerEvent<HTMLElement>) => {\n      tap.onPointerDown(e);\n      if (e.pointerType !== \"mouse\") setPressing(true);\n    },\n    onPointerUp: (e: PointerEvent<HTMLElement>) => {\n      setPressing(false);\n      tap.onPointerUp(e);\n    },\n    onPointerCancel: () => {\n      setPressing(false);\n      tap.onPointerCancel();\n    },\n    // keyboard focus holds it; the focus a tap or click leaves behind does not, or the tap that lets it go again would\n    // leave it held (the touch rules above already hold what a tap chose)\n    onFocus: (e: FocusEvent<HTMLElement>) => {\n      if ((e.target as Element).matches(\":focus-visible\")) setFocused(true);\n    },\n    onBlur: (e: FocusEvent<HTMLElement>) => {\n      if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false);\n    },\n  };\n\n  return { held: over || focused || pressing || pin !== null, handlers };\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/hooks/use-tap.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-tap.ts","content":"\"use client\";\n\nimport { useEffect, useRef, type PointerEvent, type RefObject } from \"react\";\n\n/**\n * A tap on touch or pen: down and up within 10 px of each other, so a touch that scrolls the page or swipes is not one\n * (a scroll ends in pointercancel, a swipe travels). The mouse is left to hover and click. Spread `handlers` on the\n * element; `onTap` gets the lifting event, so `e.target` says what was tapped.\n */\nexport function useTap(onTap: (e: PointerEvent<HTMLElement>) => void) {\n  const down = useRef<{ id: number; x: number; y: number } | null>(null);\n  return {\n    onPointerDown: (e: PointerEvent<HTMLElement>) => {\n      down.current = e.pointerType === \"mouse\" ? null : { id: e.pointerId, x: e.clientX, y: e.clientY };\n    },\n    onPointerUp: (e: PointerEvent<HTMLElement>) => {\n      const from = down.current;\n      down.current = null;\n      if (from && from.id === e.pointerId && Math.hypot(e.clientX - from.x, e.clientY - from.y) <= 10) onTap(e);\n    },\n    onPointerCancel: () => {\n      down.current = null;\n    },\n  };\n}\n\n/** While `on`, a touch anywhere outside `ref` calls `away`: what a tap held lasts until a touch somewhere else. */\nexport function useTouchAway(ref: RefObject<Element | null>, on: boolean, away: () => void) {\n  const latest = useRef(away);\n  useEffect(() => {\n    latest.current = away;\n  });\n  useEffect(() => {\n    if (!on) return;\n    const down = (e: globalThis.PointerEvent) => {\n      if (!ref.current?.contains(e.target as Node)) latest.current();\n    };\n    document.addEventListener(\"pointerdown\", down, true);\n    return () => document.removeEventListener(\"pointerdown\", down, true);\n  }, [on, ref]);\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":"Logo Carousel is in src/ui/beamline/components/logo-carousel/logo-carousel.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/logo-carousel"}}