{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:component","title":"Stepper","description":"Where someone is in a multi-step flow: done steps carry a check, the current one a ring, and the line between marks fills as steps complete.","categories":["navigation"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","lucide-react@^1.48.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/stepper/messages.ts","type":"registry:file","target":"src/ui/beamline/components/stepper/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  failed: \"failed\",\n  blocked: \"blocked\",\n  status: \", {status}\",\n  currentStatus: \", {status}, current step\",\n  stepOfStatus: \"Step {value} of {length}: {value2}, {status}\",\n  label: \"Progress\",\n  completeLabel: \"Complete\",\n  done: \", done\",\n  currentStep: \", current step\",\n  stepOf: \"Step {value} of {length}: {value2}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/stepper/stepper.module.css","type":"registry:file","target":"src/ui/beamline/components/stepper/stepper.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Stepper. Marks are 24px (18 compact); the connector runs between two marks and never under them, so the marks\n     need no fill of their own and sit on any surface. A done mark is a solid in the progress colour; the current one a\n     ring in it; an upcoming one a hairline ring. Done marks never rely on colour alone: they carry a check. The\n     progress colour is the accent, unless the look paints current things in its ink (--pui-current-edge: pop, whose\n     tomato accent made every done step read as an alarm). */\n  .root {\n    --_progress: var(--pui-current-edge, var(--pui-accent));\n    /* a check on that solid: the accent's own foreground, or black or white by the look's ink when it paints one (a\n       var() with no fallback leaves this unset in looks without it) */\n    --_on-progress: oklch(from var(--pui-current-edge) clamp(0.17, (0.6 - l) * 1000, 0.99) 0 0);\n    --_mark: var(--pui-size-6);\n    --_gap: var(--pui-space-2);\n    display: grid;\n    gap: var(--pui-space-3);\n    min-width: 0;\n  }\n  /* a horizontal stepper spans its row and answers to its own width (a vertical one keeps its intrinsic width) */\n  .root[data-orientation=\"horizontal\"] {\n    container-type: inline-size;\n  }\n  .root[data-compact] {\n    --_mark: var(--pui-size-4-5);\n    --_gap: var(--pui-space-1-5);\n  }\n  .list {\n    display: flex;\n    align-items: flex-start;\n    min-width: 0;\n  }\n  .root[data-orientation=\"vertical\"] > .list {\n    flex-direction: column;\n  }\n\n  .step {\n    position: relative;\n    display: flex;\n    flex: 1 1 0;\n    min-width: 0;\n  }\n  /* the last step takes only what its words need, up to a share of the row, so the line ends at its mark */\n  .root[data-orientation=\"horizontal\"] .step:last-child {\n    flex: 0 1 auto;\n    max-width: 28%;\n  }\n  .root[data-orientation=\"vertical\"] .step {\n    flex: none;\n    padding-block-end: var(--pui-space-5);\n  }\n  .root[data-orientation=\"vertical\"] .step:last-child {\n    padding-block-end: 0;\n  }\n\n  .connector {\n    position: absolute;\n    overflow: hidden;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-line);\n  }\n  .root[data-orientation=\"horizontal\"] .connector {\n    top: calc(var(--_mark) / 2 - 0.5px);\n    inset-inline: calc(var(--_mark) + var(--_gap)) var(--_gap);\n    height: 1px;\n  }\n  .root[data-orientation=\"vertical\"] .connector {\n    inset-inline-start: calc(var(--_mark) / 2 - 0.5px);\n    top: calc(var(--_mark) + var(--_gap) - 2px);\n    bottom: calc(var(--_gap) - 2px);\n    width: 1px;\n  }\n  .fill {\n    position: absolute;\n    inset: 0;\n    background: var(--_progress);\n    transform: scaleX(0);\n    transform-origin: left;\n    transition: transform var(--pui-duration-considered) var(--pui-ease-standard);\n  }\n  .fill:dir(rtl) {\n    transform-origin: right;\n  }\n  .root[data-orientation=\"vertical\"] .fill {\n    transform: scaleY(0);\n    transform-origin: top;\n  }\n  .root .fill[data-filled] {\n    transform: none;\n  }\n\n  .target {\n    display: flex;\n    flex-direction: column;\n    gap: var(--_gap);\n    min-width: 0;\n    max-width: 100%;\n    padding: 0;\n    border: 0;\n    border-radius: var(--pui-radius-chip);\n    background: none;\n    color: inherit;\n    font: inherit;\n    text-align: start;\n  }\n  .root[data-orientation=\"vertical\"] .target {\n    flex-direction: row;\n    gap: var(--pui-space-3);\n  }\n  button.target {\n    cursor: pointer;\n  }\n\n  .mark {\n    --m-color: var(--_progress);\n    position: relative;\n    display: grid;\n    flex-shrink: 0;\n    place-items: center;\n    width: var(--_mark);\n    height: var(--_mark);\n    border-radius: 50%;\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-medium);\n    box-shadow: inset 0 0 0 var(--pui-border-width) var(--pui-line-strong);\n    transition:\n      var(--m-transition),\n      box-shadow var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  /* a step that is done shows its check growing out of the middle of the mark (the value changed at once) */\n  .mark :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n  }\n  .step[data-changed] .mark :where(svg) {\n    animation: mark-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  .root[data-compact] .mark :where(svg) {\n    width: var(--pui-icon-xs);\n    height: var(--pui-icon-xs);\n  }\n  /* the step that becomes current draws its ring in */\n  .step[data-changed][data-state=\"current\"] .mark {\n    animation: ring-in var(--pui-duration-standard) var(--pui-ease-out-strong);\n  }\n  button.target:active .mark {\n    scale: var(--pui-press-scale);\n    transition-duration: 0ms;\n  }\n  .step[data-state=\"current\"] .mark {\n    color: var(--pui-current-edge, var(--pui-accent-ink));\n    box-shadow: inset 0 0 0 calc(1.5 * var(--pui-border-width)) var(--_progress);\n  }\n  .step[data-state=\"done\"] .mark {\n    color: var(--_on-progress, var(--pui-accent-foreground));\n  }\n  /* a step that did not go as planned keeps its place and says so: a red cross (failed) or an orange pause (blocked)\n     in a ring of that tone, and its label in that ink; the words are in the mark for a screen reader */\n  .step[data-status] .mark {\n    --_tone: var(--pui-red);\n    color: var(--_tone);\n    box-shadow: inset 0 0 0 calc(1.5 * var(--pui-border-width)) var(--_tone);\n  }\n  .step[data-status=\"blocked\"] .mark {\n    --_tone: var(--pui-orange);\n  }\n  .step[data-status] .label {\n    color: var(--pui-ink);\n  }\n  button.target:hover .mark {\n    transition-duration: 0ms;\n  }\n  .step[data-state=\"upcoming\"] button.target:hover .mark {\n    box-shadow: inset 0 0 0 calc(1.5 * var(--pui-border-width)) var(--pui-ink-3);\n  }\n\n  .text {\n    display: grid;\n    gap: var(--pui-space-0-5);\n    min-width: 0;\n  }\n  .root[data-orientation=\"vertical\"] .text {\n    padding-block-start: var(--pui-space-0-5);\n  }\n  .label {\n    overflow: hidden;\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    line-height: 1.35;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .root[data-compact] .label {\n    font-size: var(--pui-text-xs);\n  }\n  .step[data-state=\"upcoming\"] .label {\n    color: var(--pui-ink-3);\n  }\n  .step[data-state=\"upcoming\"] button.target:hover .label {\n    color: var(--pui-ink);\n  }\n  .meta {\n    overflow: hidden;\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-xs);\n    font-variant-numeric: tabular-nums;\n    line-height: 1.35;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .description {\n    display: -webkit-box;\n    overflow: hidden;\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    line-height: 1.35;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n  }\n\n  /* narrow horizontal: marks only in the row, the current step said under it */\n  .caption {\n    display: none;\n  }\n  @container (max-width: 30rem) {\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) .step:last-child {\n      flex: 0 0 auto;\n      max-width: none;\n    }\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) .text {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) > .caption {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: baseline;\n      gap: var(--pui-space-0-5) var(--pui-space-2);\n      min-width: 0;\n      margin: 0;\n    }\n    .caption > .label {\n      flex: 0 1 auto;\n    }\n    .caption > .description {\n      flex-basis: 100%;\n      -webkit-line-clamp: 1;\n    }\n    /* a mark pointed at or focused says its step's name under it, in place (the row keeps its marks only) */\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) .target:is(:hover, :focus-visible) > .text {\n      position: absolute;\n      top: calc(var(--_mark) + var(--pui-space-1-5));\n      inset-inline-start: 0;\n      z-index: 2;\n      width: max-content;\n      max-width: 12rem;\n      height: auto;\n      overflow: visible;\n      clip-path: none;\n      padding: var(--pui-space-1) var(--pui-space-2);\n      border-radius: var(--pui-radius-chip);\n      background: var(--pui-tooltip-bg);\n      box-shadow: var(--pui-shadow-overlay);\n      animation: caption-in var(--pui-fade-in) var(--pui-ease-default);\n    }\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) .target:is(:hover, :focus-visible) > .text > .label {\n      color: var(--pui-tooltip-fg);\n    }\n    .root[data-orientation=\"horizontal\"]:not([data-compact]) .target:is(:hover, :focus-visible) > .text > .description {\n      display: none;\n    }\n    .root[data-orientation=\"horizontal\"]:not([data-compact])\n      .step:last-child\n      .target:is(:hover, :focus-visible)\n      > .text {\n      inset-inline: auto 0;\n    }\n  }\n  /* compact labels are short: they keep the row down to about 350px */\n  @container (max-width: 22rem) {\n    .root[data-orientation=\"horizontal\"][data-compact] .step:last-child {\n      flex: 0 0 auto;\n      max-width: none;\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] .text {\n      position: absolute;\n      width: 1px;\n      height: 1px;\n      overflow: hidden;\n      clip-path: inset(50%);\n      white-space: nowrap;\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] > .caption {\n      display: flex;\n      flex-wrap: wrap;\n      align-items: baseline;\n      gap: var(--pui-space-0-5) var(--pui-space-2);\n      min-width: 0;\n      margin: 0;\n    }\n    .caption > .label {\n      flex: 0 1 auto;\n    }\n    .caption > .description {\n      flex-basis: 100%;\n      -webkit-line-clamp: 1;\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] .target:is(:hover, :focus-visible) > .text {\n      position: absolute;\n      top: calc(var(--_mark) + var(--pui-space-1-5));\n      inset-inline-start: 0;\n      z-index: 2;\n      width: max-content;\n      max-width: 12rem;\n      height: auto;\n      overflow: visible;\n      clip-path: none;\n      padding: var(--pui-space-1) var(--pui-space-2);\n      border-radius: var(--pui-radius-chip);\n      background: var(--pui-tooltip-bg);\n      box-shadow: var(--pui-shadow-overlay);\n      animation: caption-in var(--pui-fade-in) var(--pui-ease-default);\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] .target:is(:hover, :focus-visible) > .text > .label {\n      color: var(--pui-tooltip-fg);\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] .target:is(:hover, :focus-visible) > .text > .description {\n      display: none;\n    }\n    .root[data-orientation=\"horizontal\"][data-compact] .step:last-child .target:is(:hover, :focus-visible) > .text {\n      inset-inline: auto 0;\n    }\n  }\n  .count {\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-xs);\n    font-variant-numeric: tabular-nums;\n  }\n\n  .status {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-1-5);\n    margin: 0;\n    color: var(--pui-green);\n    font-size: var(--pui-text-xs);\n  }\n  .status :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n  }\n  .status[data-complete] {\n    animation: caption-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .status[data-complete] :where(svg) {\n    animation: mark-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  /* the line under a narrow row says the new step in place */\n  .caption[data-changed] {\n    animation: caption-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n}\n@keyframes mark-in {\n  from {\n    opacity: 0;\n    scale: calc(1 - 0.5 * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes ring-in {\n  from {\n    box-shadow: inset 0 0 0 calc(var(--_mark) / 2) var(--_progress);\n  }\n}\n@keyframes caption-in {\n  from {\n    opacity: 0;\n  }\n}\n"},{"path":"beamline/components/stepper/stepper.tsx","type":"registry:file","target":"src/ui/beamline/components/stepper/stepper.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useState, type CSSProperties, type ReactNode, type Ref } from \"react\";\n\nimport { Check, Pause, X } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./stepper.module.css\";\n\nexport interface StepperStep {\n  id: string;\n  label: string;\n  description?: string;\n  /** A step that did not go as planned, whatever its place: \"failed\" (it ran and did not succeed: a red cross) or\n   *  \"blocked\" (it cannot go on until something happens, an answer, a payment: an orange pause). Said in words too. */\n  status?: \"failed\" | \"blocked\";\n  /** One short line of facts under the label: when it ran, how long it took (\"09:42 · 4 s\"). */\n  meta?: ReactNode;\n}\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n\nexport interface StepperClassNames {\n  root?: string;\n  list?: string;\n  step?: string;\n  connector?: string;\n  fill?: string;\n  target?: string;\n  mark?: string;\n  text?: string;\n  label?: string;\n  description?: string;\n  meta?: string;\n  caption?: string;\n  count?: string;\n  status?: string;\n}\n\nexport interface StepperProps {\n  steps: StepperStep[];\n  /** Index of the step in progress; steps before it are done. steps.length means all are done. */\n  current: number;\n  orientation?: StepperOrientation;\n  /** Makes every step a button (jump back to edit, or move a record between stages). */\n  onStepSelect?: (index: number) => void;\n  /** Descriptions for every step, or only the current one. Default \"all\". */\n  details?: \"all\" | \"current\";\n  /** Smaller marks and text, for side panels. */\n  compact?: boolean;\n  /** The list's name. Default \"Progress\". */\n  label?: string;\n  /** Said after the last step once everything is done. Default \"Complete\". */\n  completeLabel?: string;\n  className?: string;\n  style?: CSSProperties;\n  classNames?: StepperClassNames;\n  ref?: Ref<HTMLDivElement>;\n}\n\n/** Where someone is in a multi-step flow: done steps carry a check, the current one a ring, the rest wait. The line\n *  between marks fills as steps complete; a check grows into a mark that is done and the ring draws in on the new\n *  current step. A horizontal stepper narrower than about 480px keeps only its marks in the row and says the current\n *  step, with its description, in one line under it; pointing at or focusing a mark shows its name under it, the names\n *  stay readable to screen readers and every mark stays a button. */\nexport function Stepper({\n  steps,\n  current,\n  orientation = \"horizontal\",\n  onStepSelect,\n  details = \"all\",\n  compact = false,\n  label: labelProp,\n  completeLabel: completeLabelProp,\n  className,\n  style,\n  classNames,\n  ref,\n}: StepperProps) {\n  const i18n = useMessages(\"stepper\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n  const completeLabel = completeLabelProp === undefined ? i18n(\"completeLabel\") : completeLabelProp;\n\n  const complete = current >= steps.length;\n  const now = steps[current];\n  // marks morph only for a change made while someone watches, never for the state the stepper first shows: the steps\n  // between the last current and this one are the ones that changed\n  const [move, setMove] = useState({ from: current, to: current });\n  if (move.to !== current) setMove({ from: move.to, to: current });\n  const lo = Math.min(move.from, move.to);\n  const hi = Math.max(move.from, move.to);\n  const moved = move.from !== move.to;\n  return (\n    <div\n      ref={ref}\n      style={style}\n      data-slot=\"stepper\"\n      data-orientation={orientation}\n      data-compact={compact ? \"\" : undefined}\n      className={cn(styles.root, classNames?.root, className)}\n    >\n      <ol aria-label={label} className={cn(styles.list, classNames?.list)} data-slot=\"stepper-list\">\n        {steps.map((step, i) => {\n          const state = i < current ? \"done\" : i === current ? \"current\" : \"upcoming\";\n          const flag = step.status;\n          const showDetail = step.description && !compact && (details === \"all\" || state === \"current\");\n          const Tag = onStepSelect ? \"button\" : \"div\";\n          return (\n            <li\n              key={step.id}\n              data-slot=\"stepper-step\"\n              data-state={state}\n              data-status={flag}\n              data-changed={moved && i >= lo && i <= hi ? \"\" : undefined}\n              aria-current={state === \"current\" ? \"step\" : undefined}\n              className={cn(styles.step, classNames?.step)}\n            >\n              {i < steps.length - 1 && (\n                <span aria-hidden data-slot=\"stepper-connector\" className={cn(styles.connector, classNames?.connector)}>\n                  <span\n                    className={cn(styles.fill, classNames?.fill)}\n                    data-slot=\"stepper-fill\"\n                    data-filled={i < current ? \"\" : undefined}\n                  />\n                </span>\n              )}\n              <Tag\n                {...(onStepSelect ? { type: \"button\" as const, onClick: () => onStepSelect(i) } : {})}\n                className={cn(styles.target, classNames?.target)}\n                data-slot=\"stepper-target\"\n              >\n                <span\n                  data-slot=\"stepper-mark\"\n                  className={cn(styles.mark, state === \"done\" && !flag && \"m-solid\", classNames?.mark)}\n                >\n                  {flag === \"failed\" ? (\n                    <X strokeWidth={2.5} />\n                  ) : flag === \"blocked\" ? (\n                    <Pause strokeWidth={2.5} />\n                  ) : state === \"done\" ? (\n                    <Check strokeWidth={2.5} />\n                  ) : (\n                    i18n.number(i + 1)\n                  )}\n                  <span className=\"sr-only\">\n                    {flag\n                      ? i18n(state === \"current\" ? \"currentStatus\" : \"status\", { status: i18n(flag) })\n                      : state === \"done\"\n                        ? i18n(\"done\")\n                        : state === \"current\"\n                          ? i18n(\"currentStep\")\n                          : \"\"}\n                  </span>\n                </span>\n                <span className={cn(styles.text, classNames?.text)} data-slot=\"stepper-text\">\n                  <span data-slot=\"stepper-label\" className={cn(styles.label, classNames?.label)}>\n                    {step.label}\n                  </span>\n                  {step.meta != null && (\n                    <span data-slot=\"stepper-meta\" className={cn(styles.meta, classNames?.meta)}>\n                      {step.meta}\n                    </span>\n                  )}\n                  {showDetail &&\n                    (details === \"current\" ? (\n                      // only the current step says more: it grows in, so the steps after it glide down instead of\n                      // jumping a line\n                      <span className=\"enter-grow\">\n                        <span\n                          data-slot=\"stepper-description\"\n                          className={cn(styles.description, classNames?.description)}\n                        >\n                          {step.description}\n                        </span>\n                      </span>\n                    ) : (\n                      <span data-slot=\"stepper-description\" className={cn(styles.description, classNames?.description)}>\n                        {step.description}\n                      </span>\n                    ))}\n                </span>\n              </Tag>\n            </li>\n          );\n        })}\n      </ol>\n      {!complete && now && (\n        <p\n          key={current}\n          aria-hidden\n          data-slot=\"stepper-caption\"\n          data-changed={moved ? \"\" : undefined}\n          className={cn(styles.caption, classNames?.caption)}\n        >\n          <span className={cn(styles.count, classNames?.count)} data-slot=\"stepper-count\">\n            {current + 1}/{steps.length}\n          </span>\n          <span className={cn(styles.label, classNames?.label)}>{now.label}</span>\n          {now.description && !compact && (\n            <span className={cn(styles.description, classNames?.description)}>{now.description}</span>\n          )}\n        </p>\n      )}\n      <p\n        role=\"status\"\n        data-slot=\"stepper-status\"\n        data-complete={complete ? \"\" : undefined}\n        className={cn(styles.status, !complete && \"sr-only\", classNames?.status)}\n      >\n        {complete ? (\n          <>\n            <Check />\n            {completeLabel}\n          </>\n        ) : (\n          i18n(now?.status ? \"stepOfStatus\" : \"stepOf\", {\n            value: current + 1,\n            length: steps.length,\n            value2: now?.label ?? \"\",\n            status: now?.status ? i18n(now.status) : \"\",\n          })\n        )}\n      </p>\n    </div>\n  );\n}\n\nexport default Stepper;\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":"Stepper is in src/ui/beamline/components/stepper/stepper.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/stepper"}}