{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"skeleton","type":"registry:component","title":"Skeleton","description":"Holds the shape of content while it loads, then crossfades into it, so nothing jumps.","categories":["feedback"],"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/skeleton/messages.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/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: \"Loading content\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/skeleton/motion-features.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/motion-features.ts","content":"// REASON: Motion's DOM animation features (tween and presence) load after the first paint, so a Skeleton costs a\n// small part of Motion on first load; LazyMotion in skeleton.tsx renders until they arrive.\nimport { domAnimation } from \"motion/react\";\n\nexport default domAnimation;\n"},{"path":"beamline/components/skeleton/skeleton.tsx","type":"registry:file","target":"src/ui/beamline/components/skeleton/skeleton.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useRef, type ComponentProps, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { AnimatePresence, LazyMotion, m } from \"motion/react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useHeightMorph } from \"../../hooks/use-height-morph\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { cn } from \"../../lib/utils\";\n\n// Motion's animation features arrive in a deferred chunk; the swap is instant until they land.\nconst features = () => import(\"./motion-features\").then((module) => module.default);\n\nexport interface SkeletonClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the placeholder part; no override by default. */\n  placeholder?: string;\n  /** Classes for the avatar part; no override by default. */\n  avatar?: string;\n  /** Classes for the lines part; no override by default. */\n  lines?: string;\n  /** Classes for the line part; no override by default. */\n  line?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n}\n\nexport interface SkeletonProps {\n  /** Announced to assistive tech while loading. */\n  label?: string;\n  /** Text lines in the placeholder; the last is shorter. */\n  lines?: number;\n  /** A round avatar before the lines. */\n  avatar?: boolean;\n  /** With children: the placeholder shows while true, the children after. */\n  loading?: boolean;\n  /** The real content. */\n  children?: ReactNode;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SkeletonClassNames;\n}\n\n// The sheen (utilities.css .sheen) slides a gradient layer with transform, so the compositor moves it without repainting\n// the placeholder on every frame; it pauses with the block (play state) and stops for reduced motion (data-still). A\n// scope's own motion choice (data-motion) and the OS preference also still it in CSS, so a block or group needs no\n// reader of the look's motion values: a part that only shows a placeholder stays small.\nconst SHEEN = \"sheen\";\n\n/** One placeholder shape: give it the width, height and radius of what will be there. */\nexport function SkeletonBlock({\n  className,\n  style,\n  ref,\n}: {\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<HTMLSpanElement>;\n}) {\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <span\n      ref={attach}\n      aria-hidden\n      data-slot=\"skeleton-block\"\n      data-still={reduced ? \"\" : undefined}\n      className={cn(\"block h-3 rounded-chip\", SHEEN, className)}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n    />\n  );\n}\n\n/**\n * Many placeholder shapes under one visibility and motion check: a heatmap's cells, a chart's bars. Each shape is a\n * plain element with the `sheen` class anywhere inside the group, which pauses them off screen and stills them under\n * reduced motion together, so a 500-cell placeholder is one component instead of 500, and a list's loading rows can\n * keep the real rows' structure (and so their height).\n */\nexport function SkeletonGroup({ className, style, ref, ...props }: ComponentProps<\"div\">) {\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton-group\"\n      className={className}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n      {...props}\n      // the shapes' styles key on these, so a caller's own data-slot (a chart's loading area) keeps its name\n      data-skeleton-group=\"\"\n      data-still={reduced ? \"\" : undefined}\n    />\n  );\n}\n\n/**\n * One line of text that has not arrived, inside a SkeletonGroup: an inline box exactly one line of the surrounding text\n * style tall (a zero-width character holds the line), with the shape centred in it. Put the real text's classes on it\n * (`text-sm`, `font-figure text-2xl leading-none`) and a width, and the loaded line takes its place without a shift.\n */\nexport function SkeletonLine({\n  className,\n  style,\n}: {\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}) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"skeleton-line\"\n      className={cn(\"relative inline-block w-24 align-top\", className)}\n      style={style}\n    >\n      {\"\\u200b\"}\n      <span className=\"sheen absolute inset-x-0 top-1/2 block h-[0.7em] -translate-y-1/2 rounded-chip\" />\n    </span>\n  );\n}\n\n/** Holds the shape of content while it loads, then crossfades into it; if the content is taller or shorter than its\n *  placeholder, the height eases from one to the other, so what sits below glides instead of jumping. */\nexport function Skeleton({\n  label: labelProp,\n  lines = 3,\n  avatar = false,\n  loading = true,\n  children,\n  className,\n  classNames,\n  style,\n  ref,\n}: SkeletonProps) {\n  const i18n = useMessages(\"skeleton\", 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 managedReduced = useReducedMotionSafe();\n  const reduced = motionTokens.reduced || managedReduced;\n  const showPlaceholder = children === undefined || loading;\n  // the swap keeps its path: the box eases between the placeholder's height and the content's\n  useHeightMorph(host, showPlaceholder, { clip: true, duration: \"standard\", reduced });\n  // A crossfade: reduced motion keeps it restrained, none removes it (APPEARANCE A06).\n  const fade = {\n    duration: motionTokens.mode === \"none\" ? 0 : reduced ? motionTokens.fade.in : motionTokens.duration.standard,\n    ease: motionTokens.ease.default,\n  };\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton\"\n      data-state={showPlaceholder ? \"loading\" : \"loaded\"}\n      aria-busy={showPlaceholder || undefined}\n      className={cn(\"relative\", classNames?.root, className)}\n      style={style}\n    >\n      <LazyMotion features={features}>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {showPlaceholder ? (\n            <m.div\n              key=\"placeholder\"\n              role=\"status\"\n              aria-label={label}\n              data-slot=\"skeleton-placeholder\"\n              className={cn(\"flex items-start gap-3\", classNames?.placeholder)}\n              exit={{ opacity: 0 }}\n              transition={fade}\n            >\n              {avatar && <SkeletonBlock className={cn(\"size-9 shrink-0 rounded-full\", classNames?.avatar)} />}\n              <div\n                data-slot=\"skeleton-lines\"\n                className={cn(\"flex min-w-0 flex-1 flex-col gap-2.5 py-1\", classNames?.lines)}\n              >\n                {Array.from({ length: Math.max(1, lines) }, (_, i) => (\n                  <SkeletonBlock\n                    key={i}\n                    className={classNames?.line}\n                    style={{ width: i === lines - 1 && lines > 1 ? \"62%\" : i === 0 ? \"88%\" : \"100%\" }}\n                  />\n                ))}\n              </div>\n            </m.div>\n          ) : (\n            <m.div\n              data-slot=\"skeleton-content\"\n              className={classNames?.content}\n              key=\"content\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={fade}\n            >\n              {children}\n            </m.div>\n          )}\n        </AnimatePresence>\n      </LazyMotion>\n    </div>\n  );\n}\n\nexport default Skeleton;\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-height-morph.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-height-morph.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\n/**\n * The step tray: a card whose content is replaced (a sign-in step, the sign-up card, a form step) keeps its path. When\n * `key` changes, the element's height eases from what it was to what the new content needs, so the card's edge and\n * everything under it glide instead of jumping (the morph grammar's \"modes change size in place\"). The element clips\n * while it eases, with room for focus rings, unless `clip` is false (a slot whose ease only moves what is around it, like\n * the auth page re-centring its card, shows its content whole). Nothing runs at rest; reduced motion, motion \"none\" and a card off screen\n * switch at once; a change mid-way starts from where the card is. The height twin of hooks/use-width-morph; any element whose\n * content is replaced in place uses it (the auth cards, the step form, a dialog's stepped tray, the command menu's pages).\n */\nexport function useHeightMorph(\n  ref: RefObject<HTMLElement | null>,\n  key: unknown,\n  {\n    clip = true,\n    duration = \"morph\",\n    reduced: reducedOverride = false,\n  }: {\n    clip?: boolean;\n    /** The semantic duration the ease takes: a mode change morphs, a content swap (a skeleton) takes the standard. */\n    duration?: \"morph\" | \"standard\";\n    /** Also place at once when the caller knows motion is reduced by its own policy. */\n    reduced?: boolean;\n  } = {},\n) {\n  const motion = useMotionTokens(ref);\n  const last = useRef<number | null>(null);\n  const easing = useRef<Animation | null>(null);\n  // the height it last rested at, kept current when it changes on its own (an error line, a bot check) so the next ease\n  // starts where the card really is; the ease's own frames are not a resting height\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (!easing.current) last.current = el.getBoundingClientRect().height;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const from = easing.current ? el.getBoundingClientRect().height : last.current;\n    easing.current?.cancel();\n    easing.current = null;\n    const box = el.getBoundingClientRect();\n    const to = box.height;\n    last.current = to;\n    const ms = motion.duration[duration] * 1000;\n    if (from === null || Math.abs(from - to) < 0.5 || motion.reduced || reducedOverride || ms < 2) return;\n    // off screen nobody sees the glide: the card takes its height at once and no frame is spent on it\n    if (box.bottom < 0 || box.top > innerHeight) return;\n    // the ease runs between measured heights: a keyframe to \"auto\" (even with interpolate-size, or calc-size) does not\n    // interpolate in Chromium's Web Animations, it jumps at once; at the end the element is back on its own height\n    // the clip below leaves room for a focus ring or a control's shadow at the content's edge (inert at rest); a plain\n    // length, because Chromium computes any calc() here (and so the space tokens) as 0\n    el.style.setProperty(\"overflow-clip-margin\", \"8px\");\n    const ease = motion.ease.default;\n    const overflow = clip ? \"clip\" : \"visible\";\n    const animation = el.animate(\n      [\n        { height: `${from}px`, overflow },\n        { height: `${to}px`, overflow },\n      ],\n      { duration: ms, easing: cssEase(ease, \"ease-in-out\") },\n    );\n    easing.current = animation;\n    void animation.finished.then(\n      () => {\n        if (easing.current !== animation) return;\n        easing.current = null;\n        last.current = el.getBoundingClientRect().height;\n      },\n      () => {},\n    );\n  }, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- eases on a change of the step it stands for\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":"Skeleton is in src/ui/beamline/components/skeleton/skeleton.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/skeleton"}}