{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"slot-text","type":"registry:component","title":"Slot text","description":"Text and numbers that roll into their new value on reels, digit by digit from the right; other characters fade. Read as plain text by assistive tech.","categories":["display"],"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/slot-text/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/slot-text/defaults.ts","content":"/** Pure allowed presentation defaults for SlotText; no component or runtime imports. */\nexport interface SlotTextDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"end\";\n}\n"},{"path":"beamline/components/slot-text/slot-text.module.css","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A reel stands on the run of digits it is rolling to; once it lands it is quietly reset to the same digit on its\n     middle run (no motion), so the next roll can turn either way without running out of reel. A roll is an animation\n     from the numeral it showed to the one it lands on, in whole numerals (the staircase timing in slot-text.tsx), so it\n     always starts from a whole digit, even when a new value arrives mid-roll; the two names take turns so each change\n     starts afresh. */\n  .reel {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    display: flex;\n    flex-direction: column;\n    animation-fill-mode: both;\n  }\n  .rollA {\n    animation-name: slot-text-roll-a;\n  }\n  .rollB {\n    animation-name: slot-text-roll-b;\n  }\n  /* the inline transform names the target; a landed reel is reset over it */\n  .reel[data-settled] {\n    transform: translateY(calc(var(--_rest) * -1lh)) !important;\n    animation: none !important;\n  }\n\n  /* a new place or a changed sign waits, unseen, for its turn in the ripple (its delay is the animation's length), then\n     is there whole: never half faded over its neighbour */\n  .arrive {\n    animation-name: slot-text-wait;\n    animation-timing-function: step-end;\n  }\n  @keyframes slot-text-roll-a {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-roll-b {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-wait {\n    from,\n    to {\n      visibility: hidden;\n    }\n  }\n}\n"},{"path":"beamline/components/slot-text/slot-text.tsx","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.tsx","content":"\"use client\";\n\nimport { type AnimationEvent, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\nimport { useLocale, numberFormatter } from \"../../runtime/localization\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SlotTextDefaults } from \"./defaults\";\nexport type { SlotTextDefaults } from \"./defaults\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./slot-text.module.css\";\n\nexport interface SlotTextClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the spoken part; no override by default. */\n  spoken?: string;\n  /** Classes for the visual part; no override by default. */\n  visual?: string;\n  /** Classes for the digit part; no override by default. */\n  digit?: string;\n  /** Classes for the measure part; no override by default. */\n  measure?: string;\n  /** Classes for the reel part; no override by default. */\n  reel?: string;\n  /** Classes for the numeral part; no override by default. */\n  numeral?: string;\n  /** Classes for the character part; no override by default. */\n  character?: string;\n}\n\nexport interface SlotTextProps {\n  /** Text or number shown on the animated reels. Required. */\n  value: string | number;\n  /** Turns a number into its text: currency, thousands. */\n  format?: (value: number) => string;\n  /** ms per reel. Default: twice the considered duration (about 0.7 s), following the motion scale. */\n  duration?: number;\n  /** ms between reels, right to left. Default: the word stagger (about 30 ms), following the motion scale. */\n  stagger?: number;\n  /** Extra full turns a changing digit makes. Default 0. */\n  spins?: number;\n  /** Which side stays put when the length changes. Default end (numbers). */\n  align?: \"start\" | \"end\";\n  /** Read the new value aloud when it changes. Default false (most counters are glanceable). */\n  announce?: boolean;\n  /** Content direction. Numbers stay LTR; other text resolves its own direction instead of inheriting page layout. */\n  dir?: \"ltr\" | \"rtl\" | \"auto\";\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SlotTextClassNames;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n}\n\nconst DIGITS = \"0123456789\";\n\n/** A roll of `n` numerals as a staircase on the system's ease-out: each numeral is shown whole and held until the eased\n *  travel reaches the next, so the reel slows into place like an odometer and no frame ever shows two digits split by\n *  the window (the owner rejected digits drawn over each other mid-roll, 27 Oct). */\nconst stairs = new Map<number, string>();\nfunction staircase(n: number) {\n  let s = stairs.get(n);\n  if (!s) {\n    // ease-out cubic: travel p at time t = 1 - (1 - p)^(1/3); numeral k shows once the travel passes the middle of k\n    const at = (k: number) => `${((1 - Math.cbrt(1 - (k - 0.5) / n)) * 100).toFixed(2)}%`;\n    const points = [\"0 0%\"];\n    for (let k = 1; k <= n; k++) points.push(`${(k - 1) / n} ${at(k)}`, `${k / n} ${at(k)}`);\n    points.push(\"1 100%\");\n    s = `linear(${points.join(\", \")})`;\n    stairs.set(n, s);\n  }\n  return s;\n}\n\n/** A caller's ms, or the token default, on the system's clock. */\nconst clock = (ms: number | undefined, token: string) =>\n  ms === undefined ? token : `calc(${ms}ms * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1))`;\n\n/**\n * Text and numbers that roll into their new value on reels, digit by digit from the right. A number that goes up rolls\n * every reel up and one that goes down rolls them down (9 → 0 on the way up passes through the wrap, like an odometer),\n * so the direction of the change reads before the digits do. A reel ticks through whole numerals, slowing into place, so\n * any frame taken mid-roll reads clean digits; a place the new value no longer has goes at once, and a new place or a\n * changed sign appears whole when its turn in the ripple comes. Reduced motion swaps the value at once. The reels are\n * hidden from assistive tech, which reads the value as text.\n */\nexport function SlotText({\n  value,\n  format,\n  duration,\n  stagger,\n  spins = 0,\n  align: alignProp,\n  announce = false,\n  dir,\n  className,\n  classNames,\n  ref,\n  style,\n}: SlotTextProps) {\n  const locale = useLocale();\n  const scopeDefaults = useComponentDefaults<SlotTextDefaults>(\"slot-text\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"end\";\n\n  const text =\n    typeof value === \"number\"\n      ? format\n        ? format(value)\n        : numberFormatter(locale, { maximumSignificantDigits: 21 }).format(value)\n      : value;\n  const direction = dir ?? (typeof value === \"number\" || !/\\p{L}/u.test(text) ? \"ltr\" : \"auto\");\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n  // the change is known in the same render it arrives in (no effect-delayed second pass): what it was, which way a\n  // number went, and a run counter that restarts the character fades\n  const [seen, setSeen] = useState({ text, value });\n  const [from, setFrom] = useState(text);\n  const [trend, setTrend] = useState(0);\n  const [run, setRun] = useState(0);\n  if (text !== seen.text) {\n    setSeen({ text, value });\n    setFrom(seen.text);\n    setTrend(typeof value === \"number\" && typeof seen.value === \"number\" ? Math.sign(value - seen.value) : 0);\n    setRun((r) => r + 1);\n  }\n  // when the roll has landed, the columns the old value needed and the new one does not are let go\n  const settle = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => {\n    if (from === text) return;\n    const s = tokens.duration.considered * 2 + tokens.stagger.word * Math.max(text.length, from.length);\n    const ms = Math.max((duration ?? s * 1000) + (stagger ?? 0) * Math.max(text.length, from.length), 0) + 50;\n    settle.current = setTimeout(() => setFrom(text), reduced ? 0 : ms);\n    return () => clearTimeout(settle.current);\n  }, [from, text, run, duration, stagger, reduced, tokens.duration.considered, tokens.stagger.word]);\n  // a new roll starts from where each reel rests: the resting mark from the last roll is lifted before paint\n  useLayoutEffect(() => {\n    host.current?.querySelectorAll(\"[data-settled]\").forEach((r) => r.removeAttribute(\"data-settled\"));\n  }, [run]);\n  const len = Math.max(text.length, from.length);\n  const pad = (s: string) => (align === \"end\" ? s.padStart(len, \" \") : s.padEnd(len, \" \"));\n  const now = pad(text),\n    was = pad(from);\n  // a reel holds the digits 2·spins + 3 times; it rests on the middle run, and a roll that passes the wrap lands on\n  // the run beside it, from which the reel is quietly reset to the middle once it stops\n  const base = 10 * (spins + 1);\n  const reel = Array.from({ length: 10 * (2 * spins + 3) }, (_, k) => DIGITS[k % 10]);\n  const durationCss = clock(duration, \"calc(var(--pui-duration-considered) * 2)\");\n  const staggerCss = clock(stagger, \"var(--pui-stagger-word)\");\n  const landed = (e: AnimationEvent<HTMLSpanElement>) => {\n    const target = e.target as HTMLElement;\n    if (target.dataset.slot === \"slot-text-reel\") target.setAttribute(\"data-settled\", \"\");\n  };\n  return (\n    <span\n      ref={attach}\n      data-slot=\"slot-text\"\n      data-trend={trend > 0 ? \"up\" : trend < 0 ? \"down\" : undefined}\n      dir={direction}\n      className={cn(\"relative inline-flex tabular-nums [unicode-bidi:isolate]\", classNames?.root, className)}\n      style={style}\n    >\n      <span\n        data-slot=\"slot-text-spoken\"\n        className={cn(\"sr-only\", classNames?.spoken)}\n        aria-live={announce ? \"polite\" : undefined}\n      >\n        {text}\n      </span>\n      <span\n        aria-hidden\n        data-slot=\"slot-text-visual\"\n        className={cn(\"inline-flex overflow-hidden\", classNames?.visual)}\n        onAnimationEnd={landed}\n      >\n        {[...now].map((ch, i) => {\n          const order = align === \"end\" ? len - 1 - i : i;\n          // a column keeps its element while it stays the same distance from the aligned side, so it can roll\n          const key = align === \"end\" ? `e${order}` : `s${order}`;\n          const delay = `calc(${order} * ${staggerCss})`;\n          const old = was[i];\n          // a place the new value no longer has (1,284 → 488: the \"1\" and the \",\"): gone at once, so the number takes\n          // its new width with the change itself and no half-faded digit sits over its neighbour while the reels turn\n          const padding = align === \"end\" ? i < len - text.length : i >= text.length;\n          if (padding) return null;\n          if (DIGITS.includes(ch)) {\n            const d = Number(ch);\n            const before = Number(old);\n            const changed = old !== ch && DIGITS.includes(old) && run > 0;\n            const turns = changed ? spins * 10 : 0;\n            // the way the number went decides the way every reel turns; a text value turns each the short way\n            const to =\n              !changed || reduced\n                ? base + d\n                : trend > 0\n                  ? base + d + (d > before ? 0 : 10) + turns\n                  : trend < 0\n                    ? base + d - (d < before ? 0 : 10) - turns\n                    : base + d + turns;\n            const fresh = !DIGITS.includes(old) && run > 0 && !reduced;\n            const start = changed && !reduced ? base + before : to;\n            const steps = Math.abs(to - start);\n            return (\n              <span\n                key={key}\n                data-slot=\"slot-text-digit\"\n                className={cn(\n                  \"relative inline-block h-[1lh] overflow-hidden\",\n                  fresh && styles.arrive,\n                  classNames?.digit,\n                )}\n                style={fresh ? { animationDuration: delay } : undefined}\n              >\n                <span data-slot=\"slot-text-measure\" className={cn(\"invisible\", classNames?.measure)}>\n                  0\n                </span>\n                <span\n                  data-slot=\"slot-text-reel\"\n                  className={cn(styles.reel, steps > 0 && (run % 2 ? styles.rollA : styles.rollB), classNames?.reel)}\n                  style={\n                    {\n                      \"--_from\": start,\n                      \"--_to\": to,\n                      \"--_rest\": base + d,\n                      transform: \"translateY(calc(var(--_to) * -1lh))\",\n                      ...(run > 0 &&\n                        !reduced && {\n                          animationDuration: durationCss,\n                          animationDelay: delay,\n                          animationTimingFunction: staircase(Math.max(steps, 1)),\n                        }),\n                    } as CSSProperties\n                  }\n                >\n                  {reel.map((n, k) => (\n                    <span key={k} data-slot=\"slot-text-numeral\" className={cn(\"h-[1lh]\", classNames?.numeral)}>\n                      {n}\n                    </span>\n                  ))}\n                </span>\n              </span>\n            );\n          }\n          return (\n            <span\n              key={`${key}-${ch}-${run}`}\n              data-slot=\"slot-text-character\"\n              className={cn(\n                \"inline-block whitespace-pre\",\n                !reduced && old !== ch && run > 0 && styles.arrive,\n                classNames?.character,\n              )}\n              style={{ animationDuration: delay }}\n            >\n              {ch}\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n\nexport default SlotText;\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-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":"Slot text is in src/ui/beamline/components/slot-text/slot-text.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/slot-text"}}