{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"calendar","type":"registry:component","title":"Calendar","description":"Month grids for picking a day or a range: a real grid with arrows by day and week, Page keys by month, closed days, today's dot, and a range span that follows the pointer.","categories":["forms"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-slot@1.3.3","clsx@^2.1.1","lucide-react@^1.48.0","motion@^13.4.4","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/button/button-variants.ts","type":"registry:file","target":"src/ui/beamline/components/button/button-variants.ts","content":"import { cn } from \"../../lib/utils\";\nimport styles from \"./button.module.css\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"danger\" | \"accent\";\n/** sm 32 · md 36 · lg 40 · touch 44 (a bottom action bar on a phone). */\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"touch\";\n/** Market side: buy fills with --pui-up, sell with --pui-down (a red-up market flips the two tokens, not the code). */\nexport type ButtonTone = \"up\" | \"down\";\n\n/** Which paint role paints each variant: filled variants are solids, the quiet ones keep their own light paint. */\nconst ROLE: Record<ButtonVariant, string | undefined> = {\n  primary: \"m-solid\",\n  accent: \"m-solid\",\n  secondary: \"m-control\",\n  outline: undefined,\n  ghost: undefined,\n  danger: undefined,\n};\n\n/** A variant's paint without the button's layout, for a group that wears one button's look (SplitButton paints the\n *  pair as one button and leaves its halves transparent). */\nexport function buttonPaint(variant: ButtonVariant = \"primary\", tone?: ButtonTone) {\n  return cn(ROLE[variant], styles[variant], tone && styles[tone]);\n}\n\n/** The classes that make any element look like a Button, for parts that render their own element. */\nexport function buttonClassName({\n  variant = \"primary\",\n  size = \"md\",\n  tone,\n  className,\n}: { variant?: ButtonVariant; size?: ButtonSize; tone?: ButtonTone; className?: string } = {}) {\n  return cn(styles.root, ROLE[variant], styles[variant], tone && styles[tone], styles[size], className);\n}\n"},{"path":"beamline/components/button/button.module.css","type":"registry:file","target":"src/ui/beamline/components/button/button.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Button. Every colour, size and duration is a token; a variant only sets the four local properties below. */\n  .root {\n    --_button-bg: transparent;\n    --_button-bg-hover: var(--pui-hover);\n    --_button-fg: var(--pui-ink);\n    --_button-edge: none;\n    --_button-height: var(--pui-control-height-md);\n    --_button-pad: 14px;\n    position: relative;\n    isolation: isolate;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-1-5);\n    flex-shrink: 0;\n    height: var(--_button-height);\n    min-width: var(--_button-height);\n    /* never wider than its room: in a narrow card the label ends in an ellipsis instead of spilling (the full words\n       stay the accessible name) */\n    max-width: 100%;\n    padding-inline: var(--_button-pad);\n    border: 0;\n    /* Nested context: inside a frame that sets --pui-nest-radius and --pui-nest-inset (and --pui-nest-ceiling for pills)\n       the corner is the shared nested corner (.nest-corner, utilities.css: radius − inset, or the button's own radius\n       where that would fall under the chip radius); elsewhere the control radius. --pui-button-radius wins over both. */\n    border-radius: var(--pui-button-radius, var(--pui-nested, var(--pui-radius-control)));\n    color: var(--_button-fg);\n    font-family: var(--pui-font-body);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n    line-height: 1;\n    letter-spacing: var(--pui-tracking-body);\n    white-space: nowrap;\n    text-decoration: none;\n    cursor: pointer;\n    user-select: none;\n    -webkit-tap-highlight-color: transparent;\n    /* frequency law: hover arrives at 0ms (below), leaves over the ruled departure; opacity and translate are listed\n       so a button that waits for attention (.reveal) fades away like every reveal instead of vanishing */\n    transition:\n      var(--m-transition),\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  /* the quiet variants paint themselves; filled and secondary variants are painted by their look role (m-solid,\n   m-control), which these zero-specificity rules can never override */\n  :where(.root) {\n    background: var(--_button-bg);\n    box-shadow: var(--_button-edge);\n  }\n  :where(.root:hover:not(:disabled, [aria-disabled=\"true\"])) {\n    background: var(--_button-bg-hover);\n  }\n  .root:hover:not(:disabled, [aria-disabled=\"true\"]) {\n    transition-duration: 0ms;\n  }\n  /* the asChild form has no motion: a CSS press instead */\n  .root:not(button):active {\n    transform: scale(0.97);\n  }\n  .root:disabled,\n  .root[aria-disabled=\"true\"]:not([data-loading]) {\n    opacity: var(--pui-opacity-disabled);\n    cursor: not-allowed;\n  }\n  .root[data-loading] {\n    cursor: progress;\n  }\n\n  /* Icons: 16px unless the icon says otherwise (zero specificity, so size-5 or an inline size wins) */\n  :where(.root) :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n  }\n\n  .sm {\n    --_button-height: var(--pui-control-height-sm);\n    --_button-pad: var(--pui-space-2-5);\n    /* the base gap (--pui-space-1-5); 5px had no scale step, and 6px also lets the loader's negative margin cancel\n       the gap exactly at every size */\n  }\n  .md {\n    --_button-height: var(--pui-control-height-md);\n    --_button-pad: 14px;\n  }\n  .lg {\n    --_button-height: var(--pui-control-height-lg);\n    --_button-pad: var(--pui-space-4);\n    font-size: var(--pui-text-base);\n  }\n  /* a bottom action bar on a phone: the 44 px touch target (a platform minimum, not a spacing step, so it holds when\n     the spacing parameter changes — O1 §14.2), the large label */\n  .touch {\n    --_button-height: calc(var(--pui-control-height-lg) + 0.25rem);\n    --_button-pad: var(--pui-space-5);\n    font-size: var(--pui-text-base);\n  }\n  .root[data-icon-only] {\n    padding-inline: 0;\n    width: var(--_button-height);\n  }\n\n  /* the main action: ink on the page, inverted per theme */\n  .primary {\n    --m-color: var(--pui-ink);\n    --_button-fg: var(--pui-ink-inverse);\n  }\n  .secondary {\n    --_button-fg: var(--pui-ink);\n  }\n  .outline {\n    --_button-edge: inset 0 0 0 1px var(--pui-line-strong);\n    --_button-bg-hover: var(--pui-hover);\n  }\n  .ghost {\n    --_button-fg: var(--pui-ink-2);\n    --_button-bg-hover: var(--pui-hover);\n  }\n  .ghost:hover:not(:disabled) {\n    color: var(--pui-ink);\n  }\n  .danger {\n    --_button-fg: var(--pui-red);\n    --_button-edge: inset 0 0 0 1px color-mix(in oklab, var(--pui-red) 42%, transparent);\n    --_button-bg-hover: var(--pui-red-tint);\n  }\n  .accent {\n    --m-color: var(--pui-accent);\n    --_button-fg: var(--pui-accent-foreground);\n  }\n\n  /* Market side. Declared after the variants so it wins: a filled variant takes the side's colour and its on-colour\n     ink; a quiet variant keeps its paint and colours the label. */\n  .up {\n    --m-color: var(--pui-up);\n    --_button-fg: var(--pui-up);\n  }\n  .down {\n    --m-color: var(--pui-down);\n    --_button-fg: var(--pui-down);\n  }\n  .ghost:is(.up, .down):hover:not(:disabled) {\n    color: var(--_button-fg);\n  }\n  .primary.up,\n  .accent.up {\n    --_button-fg: var(--pui-on-up);\n  }\n  .primary.down,\n  .accent.down {\n    --_button-fg: var(--pui-on-down);\n  }\n\n  /* The main actions' light, where the look provides one, as a spectrum. Glow spills a soft light in the brand's\n     spectrum from under the button's bottom edge (clipped at it, so it never tints the face) that brightens under the\n     pointer. Prism keeps it inside the glass (--m-solid-glow-fit: 1): the face the light leaves by, the bottom edge,\n     splits it into a crisp line with a faint wash rising into the face, only while pointed at. Every other look\n     leaves --m-solid-glow at none, so the layer does not exist. */\n  .primary::after,\n  .accent::after {\n    --_fit: var(--m-solid-glow-fit, 0);\n    content: var(--m-solid-glow);\n    position: absolute;\n    z-index: -1;\n    top: calc((1 - var(--_fit)) * (100% + var(--m-solid-glow-gap, 0px)));\n    inset-inline: var(--m-solid-glow-inset, 14%);\n    height: var(--m-solid-glow-height, 14px);\n    border-radius: calc(\n      var(--_fit) * var(--pui-button-radius, var(--pui-nested, var(--pui-radius-control))) + (1 - var(--_fit)) *\n        var(--pui-radius-pill)\n    );\n    background: var(--m-spectrum);\n    background-size: var(--m-spectrum-size, 100% 100%);\n    /* the part of the spectrum that shows: this button's own angle (--m-prism-seed, new at every press), turned by\n       where the pointer is across the face (--m-prism-x) in a look that turns it (--m-solid-glow-turn) */\n    background-position: calc((var(--m-prism-seed, 0.3) + var(--m-prism-x, 0.5) * var(--m-solid-glow-turn, 0)) * 100%) 0;\n    filter: blur(var(--m-solid-glow-blur, 9px));\n    clip-path: var(--m-solid-glow-clip, inset(0 -32px -32px -32px));\n    mask: var(--m-solid-glow-mask, none);\n    mask-position:\n      0 0,\n      calc(var(--m-prism-seed, 0.3) * 100%) 0;\n    mask-composite: intersect;\n    opacity: var(--m-solid-glow-rest, 0.75);\n    scale: var(--m-solid-glow-scale, 1) 1;\n    pointer-events: none;\n    transition:\n      opacity var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default),\n      background-position var(--pui-duration-considered) var(--pui-ease-default),\n      mask-position var(--pui-duration-considered) var(--pui-ease-default);\n  }\n  /* pointed at: the light brightens (glow) or grows out from the centre and drifts along while the pointer stays\n     (prism) */\n  .primary:hover::after,\n  .accent:hover::after {\n    opacity: 1;\n    scale: 1 1;\n    transition-duration: var(--m-solid-glow-in, 0ms);\n    animation: var(--m-solid-glow-flow, none);\n  }\n  /* A change of word or colour (Save → Saved, a tone turning green) passes a band of split light across the face once,\n     in a look that has one (prism: --m-solid-shift-light; the pen hook's ::before is free on a solid otherwise). */\n  .primary[data-shift]::before,\n  .accent[data-shift]::before {\n    content: var(--m-solid-shift-light, none);\n    position: absolute;\n    inset: 0;\n    z-index: -1;\n    border: 0;\n    border-radius: inherit;\n    background: var(--m-spectrum) 0 0 / var(--m-spectrum-size, 100% 100%);\n    mask: linear-gradient(105deg, transparent 39%, #000 47.5%, #000 52.5%, transparent 61%) 0 0 / 300% 100% no-repeat;\n    opacity: 0;\n    animation: prism-shift calc(var(--pui-duration-considered) * 2.2) var(--pui-ease-default);\n    pointer-events: none;\n  }\n  @keyframes prism-shift {\n    from {\n      mask-position: 100% 0;\n      opacity: 0.25;\n    }\n    35% {\n      opacity: 0.55;\n    }\n    to {\n      mask-position: 0 0;\n      opacity: 0;\n    }\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .primary::after,\n    .accent::after {\n      transition: none;\n      animation: none !important;\n    }\n    .primary[data-shift]::before,\n    .accent[data-shift]::before {\n      content: none;\n    }\n  }\n  .root:disabled::after,\n  .root[aria-disabled=\"true\"]::after {\n    content: none;\n  }\n\n  /* The loader opens its own width, and the negative margin cancels the gap so the label does not jump */\n  .loader {\n    display: inline-flex;\n    overflow: hidden;\n    margin-inline-end: calc(var(--pui-space-1-5) * -1);\n  }\n  .loader > * {\n    margin-inline-end: var(--pui-space-1-5);\n  }\n  .spinner {\n    color: currentColor;\n  }\n  .root[data-icon-only] .loader {\n    margin-inline-end: 0;\n  }\n  .root[data-icon-only] .loader > * {\n    margin-inline-end: 0;\n  }\n\n  /* The label holds its place while the text inside swaps; the leaving text pops out of flow against it */\n  .label {\n    position: relative;\n    display: inline-flex;\n    justify-content: center;\n    min-width: 0;\n    overflow: visible;\n  }\n  /* the clip box is let out by a little over a line so descenders are never cut, without changing the layout */\n  .text {\n    display: inline-block;\n    max-width: 100%;\n    margin-block: -0.2em;\n    padding-block: 0.2em;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .label[data-morphing] > .text {\n    overflow: visible;\n    text-overflow: clip;\n  }\n}\n\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .root:not(button):active,\n  :global(.pui[data-motion=\"none\"]) .root:not(button):active {\n    transform: none;\n  }\n  @media (prefers-reduced-motion: reduce) {\n    .root:not(button):active {\n      transform: none;\n    }\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: nest-corner from global;\n}\n"},{"path":"beamline/components/button/button.tsx","type":"registry:file","target":"src/ui/beamline/components/button/button.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport {\n  Children,\n  Fragment,\n  isValidElement,\n  useLayoutEffect,\n  useRef,\n  type ButtonHTMLAttributes,\n  type PointerEvent,\n  type ReactNode,\n  type Ref,\n  type RefObject,\n  type ComponentProps,\n} from \"react\";\nimport { AnimatePresence, LazyMotion, m, spring, useIsPresent } from \"motion/react\";\nimport { animate } from \"motion/mini\";\nimport * as Slot from \"@radix-ui/react-slot\";\nimport { Spinner } from \"../spinner/spinner\";\nimport type { BuiltInLook as Look } from \"../../runtime/preset\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./button.module.css\";\n\nexport type { ButtonVariant, ButtonSize, ButtonTone } from \"./button-variants\";\nexport { buttonPaint, buttonClassName } from \"./button-variants\";\nimport { buttonClassName, type ButtonVariant, type ButtonSize, type ButtonTone } from \"./button-variants\";\nimport type { ButtonDefaults } from \"./defaults\";\nexport type { ButtonDefaults } from \"./defaults\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\ntype NativeProps = Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  \"onDrag\" | \"onDragEnd\" | \"onDragStart\" | \"onAnimationStart\"\n>;\n\nexport interface ButtonProps extends NativeProps {\n  /** Visual weight: primary (ink-filled), secondary, outline, ghost, danger, or accent (filled with the brand). */\n  variant?: ButtonVariant;\n  /** Height 32 / 36 / 40 px; touch 44 px for a bottom action bar on a phone. */\n  size?: ButtonSize;\n  /** Market side. On primary/accent the fill becomes --pui-up (buy) or --pui-down (sell); on the quiet variants the\n   *  label takes that colour. Pair it with a word or arrow: colour is never the only signal. */\n  tone?: ButtonTone;\n  /** Shows a loader in front of the label, sets aria-busy and ignores clicks; size and focus stay. */\n  loading?: boolean;\n  classNames?: Partial<Record<\"root\" | \"label\" | \"loader\", string>>;\n  /** Paint this button in another look than the one in force around it (DESIGN-LANGUAGE.md \"Looks\"). */\n  look?: Look;\n  /** Render the single child (a link, a Radix trigger) with the button's look instead of a <button>. */\n  asChild?: boolean;\n  /** A square button holding only an icon (give it an aria-label). Decided by the children when left out: icon-only when\n   *  no words sit anywhere among them (an icon, or a frame of icons), so `<span>Close</span>` is a label. */\n  iconOnly?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n// Motion's animation features arrive in a deferred chunk (AT11): the button renders and works at once, and its press,\n// loader and label motion run once they land, a frame or two after mount. Buttons that mount after that get the\n// loaded features directly, so they render once instead of again when a lazy load resolves.\ntype Features = typeof import(\"./motion-features\").default;\nlet loaded: Features | null = null;\nconst features = () =>\n  import(\"./motion-features\").then((module) => {\n    loaded = module.default;\n    return loaded;\n  });\n\n/** Children with fragments opened up, so `{busy ? <><Icon />Saving</> : …}` reads as icon plus text. */\nconst flat = (children: ReactNode): ReactNode[] =>\n  Children.toArray(children).flatMap((c) =>\n    isValidElement<{ children?: ReactNode }>(c) && c.type === Fragment ? flat(c.props.children) : [c],\n  );\n\n/** Whether any words sit anywhere inside (an icon, or a frame of icons, has none). */\nfunction hasText(node: ReactNode): boolean {\n  if (typeof node === \"string\") return node.trim() !== \"\";\n  if (typeof node === \"number\") return true;\n  if (Array.isArray(node)) return node.some(hasText);\n  return isValidElement<{ children?: ReactNode }>(node) && hasText(node.props.children);\n}\n\n/** Splits children into the text run (the label that can change) and everything else (icons), in order, and says\n *  whether any element carries content of its own (a `<span>Close</span>` label, unlike an icon). */\nfunction split(children: ReactNode) {\n  const parts = flat(children);\n  const text = parts\n    .filter((p) => typeof p === \"string\" || typeof p === \"number\")\n    .join(\"\")\n    .trim();\n  const firstText = parts.findIndex((p) => typeof p === \"string\" || typeof p === \"number\");\n  const before = firstText === -1 ? parts : parts.slice(0, firstText).filter(isValidElement);\n  const after = firstText === -1 ? [] : parts.slice(firstText).filter(isValidElement);\n  const content = parts.some((p) => isValidElement<{ children?: ReactNode }>(p) && hasText(p.props.children));\n  return { text, before, after, content };\n}\n\n/**\n * The action control. Hover answers at once and lets go over --pui-motion-out; a press scales down at once, or swells\n * on the snappy spring where the look's --pui-press-scale is above 1 (glass), never on a trigger that anchors a\n * floating layer, which would measure a resized box; a changed label slides in while the\n * width follows on a critically damped spring; loading keeps the size and the focus.\n */\nexport function Button({\n  variant: variantProp,\n  size: sizeProp,\n  tone: toneProp,\n  loading = false,\n  look,\n  asChild = false,\n  iconOnly: iconOnlyProp,\n  className,\n  classNames,\n  children,\n  type,\n  onClick,\n  onPointerMove,\n  onPointerDown,\n  disabled,\n  ref,\n  ...props\n}: ButtonProps) {\n  const i18n = useMessages(\"button\", englishMessages);\n\n  const defaults = useComponentDefaults<ButtonDefaults>(\"button\", [\"variant\", \"size\", \"tone\"]);\n  const variant = variantProp ?? defaults.variant ?? \"primary\";\n  const size = sizeProp ?? defaults.size ?? \"md\";\n  const tone = toneProp ?? defaults.tone;\n  const host = useRef<HTMLButtonElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const requestedReduced = useReducedMotionSafe();\n  const reduced = requestedReduced || motionTokens.reduced;\n  // Reduced motion keeps a restrained fade; none removes it (APPEARANCE A06).\n  const fade = motionTokens.mode === \"none\" ? 0 : motionTokens.fade.in;\n  // a caller's aria-disabled keeps the button focusable (focus never drops after Save resolves) but inert to presses\n  const softDisabled = props[\"aria-disabled\"] === true || props[\"aria-disabled\"] === \"true\";\n  const { text, before, after, content } = split(children);\n  const prism = usePrismLight(host, variant === \"primary\" || variant === \"accent\", `${text}|${tone}|${variant}`);\n  // with asChild the text sits inside the child (the link), not among our own children\n  const inner = asChild && isValidElement<{ children?: ReactNode }>(children) ? split(children.props.children) : null;\n  const iconOnly = iconOnlyProp ?? (inner ? !inner.text && !inner.content : !text && !content);\n  const anchor =\n    (props[\"aria-haspopup\"] != null && props[\"aria-haspopup\"] !== false && props[\"aria-haspopup\"] !== \"false\") ||\n    \"data-state\" in props;\n  const shared = {\n    \"data-slot\": \"button\",\n    \"data-variant\": variant,\n    \"data-size\": size,\n    \"data-tone\": tone,\n    \"data-icon-only\": iconOnly ? \"\" : undefined,\n    \"data-loading\": loading ? \"\" : undefined,\n    \"data-look\": look,\n    className: buttonClassName({ variant, size, tone, className: cn(classNames?.root, className) }),\n  };\n\n  if (asChild)\n    return (\n      <Slot.Root {...shared} {...props} ref={attach}>\n        {children}\n      </Slot.Root>\n    );\n\n  const button = (\n    <m.button\n      ref={attach}\n      type={type ?? \"button\"}\n      {...shared}\n      {...props}\n      disabled={disabled}\n      aria-busy={loading || undefined}\n      aria-disabled={loading || softDisabled || undefined}\n      onPointerMove={(e) => {\n        prism.turn(e);\n        onPointerMove?.(e);\n      }}\n      onPointerDown={(e) => {\n        prism.press();\n        onPointerDown?.(e);\n      }}\n      onClick={(e) => {\n        if (loading || softDisabled) {\n          e.preventDefault();\n          return;\n        }\n        onClick?.(e);\n      }}\n      whileTap={\n        reduced || anchor || disabled || loading || softDisabled\n          ? undefined\n          : {\n              scale: motionTokens.pressScale,\n              // a press that sinks lands at once; one that swells toward the finger (glass) grows on the spring\n              transition: motionTokens.pressScale > 1 ? motionTokens.spring.snappy : { duration: 0 },\n            }\n      }\n      transition={{ scale: motionTokens.spring.snappy }}\n    >\n      <AnimatePresence initial={false}>\n        {loading && (\n          <m.span\n            key=\"loader\"\n            data-slot=\"button-loader\"\n            className={cn(styles.loader, classNames?.loader)}\n            initial={reduced ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            animate={{ width: \"auto\", opacity: 1 }}\n            exit={reduced ? { opacity: 0 } : { width: 0, opacity: 0 }}\n            transition={{ duration: reduced ? fade : motionTokens.duration.standard, ease: motionTokens.ease.default }}\n          >\n            <Spinner size=\"sm\" label={i18n(\"working\")} className={styles.spinner} />\n          </m.span>\n        )}\n      </AnimatePresence>\n      {iconOnly ? !loading && before : before}\n      {!iconOnly && text && (\n        <Label text={text} reduced={!!reduced} fade={fade} motionTokens={motionTokens} className={classNames?.label} />\n      )}\n      {after}\n    </m.button>\n  );\n  return <LazyMotion features={loaded ?? features}>{button}</LazyMotion>;\n}\n\n/**\n * The main action's light inside the glass, in a look that draws one (prism): its own angle from the start, turned by\n * where the pointer is across the face, a new angle at every press, and a sweep of split light (data-shift) when the\n * button's word, tone or variant changes after it first appears. Writes two style properties and one attribute on the\n * button; nothing runs at rest. Looks without this light ignore all three.\n */\nfunction usePrismLight(host: RefObject<HTMLButtonElement | null>, solid: boolean, state: string) {\n  const frame = useRef(0);\n  const shown = useRef(state);\n  const ending = useRef(0);\n  useLayoutEffect(() => {\n    const el = host.current;\n    if (el && solid) el.style.setProperty(\"--m-prism-seed\", Math.random().toFixed(3));\n  }, [host, solid]);\n  useLayoutEffect(() => {\n    const el = host.current;\n    if (!el || !solid || shown.current === state) return;\n    shown.current = state;\n    // restart the sweep if it is still running\n    delete el.dataset.shift;\n    void el.offsetWidth;\n    el.dataset.shift = \"\";\n    clearTimeout(ending.current);\n    ending.current = window.setTimeout(() => delete el.dataset.shift, 1200);\n  }, [host, solid, state]);\n  useLayoutEffect(\n    () => () => {\n      clearTimeout(ending.current);\n      cancelAnimationFrame(frame.current);\n    },\n    [],\n  );\n  return {\n    turn: (e: PointerEvent<HTMLButtonElement>) => {\n      if (!solid || e.pointerType !== \"mouse\" || frame.current) return;\n      const el = e.currentTarget;\n      const x = e.clientX;\n      frame.current = requestAnimationFrame(() => {\n        frame.current = 0;\n        const r = el.getBoundingClientRect();\n        if (r.width) el.style.setProperty(\"--m-prism-x\", ((x - r.left) / r.width).toFixed(3));\n      });\n    },\n    press: () => {\n      if (solid) host.current?.style.setProperty(\"--m-prism-seed\", Math.random().toFixed(3));\n    },\n  };\n}\n\n/** The text run. When it changes, the old words leave upward and the new ones rise in, while the width moves from the\n *  old measure to the new one; no measuring happens while the label is unchanged. */\nfunction Label({\n  text,\n  reduced,\n  fade,\n  motionTokens,\n  className,\n}: {\n  text: string;\n  reduced: boolean;\n  /** Seconds of the label crossfade under reduced motion (0 under none). */\n  fade: number;\n  /** The button's own, resolved at the button: the label sits inside it, in the same look and motion. */\n  motionTokens: ReturnType<typeof useMotionTokens>;\n  className?: string;\n}) {\n  const slot = useRef<HTMLSpanElement>(null);\n  const latest = useRef(motionTokens);\n  useLayoutEffect(() => {\n    latest.current = motionTokens;\n  }, [motionTokens]);\n  const last = useRef<{ text: string; width: number } | null>(null);\n  useLayoutEffect(() => {\n    const el = slot.current;\n    if (!el) return;\n    const prev = last.current;\n    // first mount: read the width after paint, when layout is already clean, instead of forcing a layout per button\n    if (!prev) {\n      last.current = { text, width: Number.NaN };\n      const id = requestAnimationFrame(() => {\n        if (last.current?.text === text) last.current = { text, width: el.getBoundingClientRect().width };\n      });\n      return () => cancelAnimationFrame(id);\n    }\n    el.style.width = \"\";\n    const width = el.getBoundingClientRect().width;\n    last.current = { text, width };\n    // a label that is not rendered (a hidden toolbar on a phone) has nothing to animate\n    if (prev.text === text || reduced || Number.isNaN(prev.width) || !el.getClientRects().length) return;\n    // The same spring, played natively: motion/mini samples the generator into a linear() easing.\n    const { visualDuration, bounce } = latest.current.spring.morph;\n    // while the width eases the words are clipped, not cut short with an ellipsis (that is for a room too narrow)\n    el.dataset.morphing = \"\";\n    const run = animate(el, { width: [`${prev.width}px`, `${width}px`] }, { type: spring, visualDuration, bounce });\n    run.then(() => {\n      el.style.width = \"\";\n      delete el.dataset.morphing;\n    });\n    // cancel, not stop: stop commits the running value, which throws once the element stops being rendered, and the\n    // next change measures afresh anyway\n    return () => {\n      run.cancel();\n      el.style.width = \"\";\n      delete el.dataset.morphing;\n    };\n  }, [text, reduced]);\n  // A label that changes faster than its words can rise in (a count read live while a hand drags) shows each new\n  // value at once: otherwise every value would start invisible and the button would read blank for the whole drag.\n  const rapid = useRapid(text, motionTokens.duration.standard * 1000);\n  const enter = reduced ? { opacity: 0 } : { opacity: 0, y: \"0.45em\", filter: `blur(${motionTokens.blur.subtle}px)` };\n  const leave = reduced\n    ? { opacity: 0 }\n    : {\n        opacity: 0,\n        y: \"-0.45em\",\n        filter: `blur(${motionTokens.blur.subtle}px)`,\n        transition: { duration: motionTokens.duration.fast },\n      };\n  return (\n    <span ref={slot} data-slot=\"button-label\" className={cn(styles.label, className)}>\n      <AnimatePresence mode=\"popLayout\" initial={false} custom={rapid}>\n        <Words\n          key={text}\n          className={styles.text}\n          custom={rapid}\n          variants={{\n            enter: (fast: boolean) => (fast ? { opacity: 1, y: \"0em\", filter: \"blur(0px)\" } : enter),\n            shown: { opacity: 1, y: \"0em\", filter: \"blur(0px)\" },\n            leave: (fast: boolean) => (fast ? { opacity: 0, transition: { duration: 0 } } : leave),\n          }}\n          initial=\"enter\"\n          animate=\"shown\"\n          exit=\"leave\"\n          transition={{ duration: reduced ? fade : motionTokens.duration.standard, ease: motionTokens.ease.default }}\n        >\n          {text}\n        </Words>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/** Whether `text` changed again within `ms` of its last change. Read in render: the answer belongs to this text. */\nfunction useRapid(text: string, ms: number) {\n  const seen = useRef({ text, at: Number.NEGATIVE_INFINITY, rapid: false });\n  if (seen.current.text !== text) {\n    const at = performance.now();\n    seen.current = { text, at, rapid: at - seen.current.at < ms };\n  }\n  return seen.current.rapid;\n}\n\n/** One label's words. Leaving, they are already gone for assistive tech: the button's name is the new label at once,\n *  not both while the old words animate away (or for good, where time stands still in a test). */\nfunction Words(props: ComponentProps<typeof m.span>) {\n  const present = useIsPresent();\n  return <m.span {...props} aria-hidden={present ? undefined : true} />;\n}\n\nexport default Button;\n"},{"path":"beamline/components/button/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/button/defaults.ts","content":"import type { ButtonSize, ButtonTone, ButtonVariant } from \"./button-variants\";\n/** Scoped design choices only; no loading, events or domain state. */\nexport interface ButtonDefaults {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  tone?: ButtonTone;\n}\n"},{"path":"beamline/components/button/messages.ts","type":"registry:file","target":"src/ui/beamline/components/button/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  working: \"Working\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/button/motion-features.ts","type":"registry:file","target":"src/ui/beamline/components/button/motion-features.ts","content":"// REASON: Motion's DOM animation features (tween, spring, presence, press) are loaded after the first paint, so a\n// Button costs a small part of Motion on first load; LazyMotion in button.tsx renders until they arrive.\nimport { domAnimation } from \"motion/react\";\n\nexport default domAnimation;\n"},{"path":"beamline/components/calendar/calendar.module.css","type":"registry:file","target":"src/ui/beamline/components/calendar/calendar.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Arrows sit above the month grid; the space between them leaves the title reachable. */\n  .navigation {\n    z-index: 1;\n    pointer-events: none;\n  }\n  .navigation > button {\n    pointer-events: auto;\n  }\n  /* The day's square: 36px, or a seventh of a narrower room (a side panel, a small card) down to 28px, so a month\n     never spills its room (cqi reads the nearest size container; with none it is the viewport and the days keep 36px). */\n  .months {\n    --_cell: clamp(28px, calc((100cqi - var(--pui-space-4)) / 7), 36px);\n  }\n  .square {\n    width: var(--_cell);\n    height: var(--_cell);\n  }\n  /* A day: a square with the date in tabular figures. Disabled days are struck through; the chosen day is a solid. */\n  .day {\n    position: relative;\n    display: grid;\n    width: var(--_cell);\n    height: var(--_cell);\n    place-items: center;\n    border-radius: var(--pui-radius-control);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-sm);\n    font-variant-numeric: tabular-nums;\n    outline-offset: -2px;\n    /* hover arrives at 0 ms and leaves over --pui-motion-out; a press is acknowledged at 0 ms */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .day:hover,\n  .day:active {\n    transition: none;\n  }\n  .day:active {\n    scale: var(--pui-press-scale);\n  }\n  .free {\n    color: var(--pui-ink-2);\n  }\n  .free:hover {\n    background: var(--pui-hover);\n    color: var(--pui-ink);\n  }\n  /* inside a chosen range */\n  .between {\n    color: var(--pui-ink);\n  }\n  .between:hover {\n    background: var(--pui-hover-2);\n  }\n  .on {\n    position: relative;\n    z-index: 1;\n    color: var(--pui-accent-foreground);\n  }\n  /* while the solid is still on its way the new day keeps the page's ink, so its number never disappears */\n  [data-slot=\"calendar-months\"]:has(> [data-morphing]) .on:not(.m-solid) {\n    color: var(--pui-ink);\n  }\n  /* the chosen day's solid, apart from the day so it can travel to the next one picked (calendar.tsx, contour) */\n  .chosen {\n    position: absolute;\n    top: 0;\n    left: 0;\n    border-radius: var(--pui-radius-control);\n    pointer-events: none;\n  }\n  .day:disabled {\n    color: var(--pui-ink-3);\n    text-decoration-line: line-through;\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n\n  /* the month title: it opens the years (and from the months, the years again) */\n  .title {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-1);\n    height: var(--pui-control-height-sm);\n    padding-inline: var(--pui-space-2-5) var(--pui-space-2);\n    border-radius: var(--pui-radius-control);\n    color: var(--pui-ink);\n    font-weight: var(--pui-weight-medium);\n    outline-offset: -2px;\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .title:hover {\n    background: var(--pui-hover);\n    transition: none;\n  }\n  .title svg {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    color: var(--pui-ink-3);\n    transition: rotate var(--pui-duration-feedback) var(--pui-ease-out-strong);\n  }\n  .title[aria-expanded=\"true\"] svg {\n    rotate: 180deg;\n  }\n  /* years or months in the space the days took, at the same width */\n  .picker {\n    display: grid;\n    align-content: start;\n    gap: var(--pui-space-1);\n    height: calc(var(--_cell) * 7);\n    padding: var(--pui-space-0-5);\n    overflow-y: auto;\n    overscroll-behavior: contain;\n    scrollbar-width: thin;\n  }\n  .years {\n    grid-template-columns: repeat(4, minmax(0, 1fr));\n    grid-auto-rows: 40px;\n  }\n  .monthGrid {\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n    grid-auto-rows: 58px;\n  }\n  .pick {\n    position: relative;\n    display: grid;\n    place-items: center;\n    border-radius: var(--pui-radius-control);\n    color: var(--pui-ink-2);\n    font-family: var(--pui-font-numeric);\n    font-size: var(--pui-text-sm);\n    font-variant-numeric: tabular-nums;\n    outline-offset: -2px;\n  }\n  .pick {\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .pick:not(.pickOn):hover {\n    background: var(--pui-hover);\n    color: var(--pui-ink);\n    transition: none;\n  }\n  .pickOn {\n    color: var(--pui-accent-foreground);\n  }\n  .pick:disabled {\n    color: var(--pui-ink-3);\n    opacity: var(--pui-opacity-disabled);\n    pointer-events: none;\n  }\n  .pickToday {\n    position: absolute;\n    bottom: 5px;\n    width: 4px;\n    height: 4px;\n    border-radius: 50%;\n    background: var(--pui-accent);\n  }\n  .pickOn .pickToday {\n    background: var(--pui-accent-foreground);\n  }\n  /* A page turn (the arrows, Page Up/Down, focus walking off the month): the new month arrives from the side it lies\n     on, 10px over --pui-duration-feedback, so the eye knows which way it went. Two identical keyframes take turns so\n     each turn restarts the arrival without remounting the grid (focus and the chosen day's solid stay). Reduced\n     motion: no travel (--pui-motion-travel 0) and the duration token is 1 ms. Live data is not involved: a pick never\n     moves anything but the solid. */\n  .months:dir(rtl) {\n    --_flip: -1;\n  }\n  .months[data-turn=\"a\"] > [data-slot=\"calendar-month\"] {\n    animation: turn-a var(--pui-duration-feedback) var(--pui-ease-out-strong) both;\n  }\n  .months[data-turn=\"b\"] > [data-slot=\"calendar-month\"] {\n    animation: turn-b var(--pui-duration-feedback) var(--pui-ease-out-strong) both;\n  }\n  @keyframes turn-a {\n    from {\n      opacity: 0.4;\n      translate: calc(10px * var(--_dir, 1) * var(--_flip, 1) * var(--pui-motion-travel, 1)) 0;\n    }\n  }\n  @keyframes turn-b {\n    from {\n      opacity: 0.4;\n      translate: calc(10px * var(--_dir, 1) * var(--_flip, 1) * var(--pui-motion-travel, 1)) 0;\n    }\n  }\n  /* a view swap: the new view fades up from 98% (reduced motion: the duration token is 1 ms) */\n  .viewIn {\n    animation: calendar-view-in var(--pui-duration-feedback) var(--pui-ease-out-strong) both;\n  }\n  @keyframes calendar-view-in {\n    from {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n  /* how many records a day holds: dots in the day's own ink (a count, one hue), three and a plus past that */\n  .marks {\n    position: absolute;\n    bottom: 3px;\n    display: flex;\n    align-items: center;\n    gap: 2px;\n    color: inherit;\n    pointer-events: none;\n  }\n  .markDot {\n    width: 4px;\n    height: 4px;\n    border-radius: 50%;\n    background: currentColor;\n  }\n  .markMore {\n    font-size: 8px;\n    line-height: 1;\n  }\n  /* a day never recorded: faint and hatched, never read as a day with none */\n  .day[data-unrecorded] {\n    color: var(--pui-ink-3);\n    background-image: repeating-linear-gradient(\n      135deg,\n      color-mix(in oklab, var(--pui-ink) 8%, transparent) 0 1px,\n      transparent 1px 5px\n    );\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.chosen {\n  composes: m-solid from global;\n}\n"},{"path":"beamline/components/calendar/calendar.tsx","type":"registry:file","target":"src/ui/beamline/components/calendar/calendar.tsx","content":"\"use client\";\nimport { dateFormatter, useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useLocale } from \"../../runtime/localization\";\n\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport type { CSSProperties, Ref } from \"react\";\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\nimport { ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../button/button\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useIndicator } from \"../../hooks/use-indicator\";\nimport { useContour } from \"../../hooks/use-contour\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { useToday } from \"../../lib/now\";\nimport styles from \"./calendar.module.css\";\n\nexport interface DateRange {\n  /** First date included in the range. Required. */\n  start: Date;\n  /** Last date included in the range. Required. */\n  end: Date;\n}\n/** Days that cannot be picked: a list, or a test. */\nexport type CalendarDateMatcher = Date[] | ((date: Date) => boolean);\n\nconst DAY = 86400000;\nexport const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nexport const sameDay = (a?: Date | null, b?: Date | null) =>\n  !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst addMonths = (d: Date, n: number) => {\n  const t = new Date(d.getFullYear(), d.getMonth() + n, 1);\n  return new Date(\n    t.getFullYear(),\n    t.getMonth(),\n    Math.min(d.getDate(), new Date(t.getFullYear(), t.getMonth() + 1, 0).getDate()),\n  );\n};\nconst firstOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);\n\nexport type CalendarClassNames = Partial<\n  Record<\n    | \"month\"\n    | \"monthTitle\"\n    | \"grid\"\n    | \"weekday\"\n    | \"blank\"\n    | \"cell\"\n    | \"day\"\n    | \"today\"\n    | \"root\"\n    | \"navigation\"\n    | \"months\"\n    | \"announcement\"\n    | \"title\"\n    | \"picker\"\n    | \"pick\",\n    string\n  >\n>;\n\ninterface Common {\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: CalendarClassNames;\n  /** Which month shows first (controlled), and its change. */\n  month?: Date;\n  /** Initial month; defaults to the selected day or range start, then today within the allowed bounds. */\n  defaultMonth?: Date;\n  /** Reports month after a change; no callback by default. */\n  onMonthChange?: (month: Date) => void;\n  /** The first day that can be picked: a date, or \"today\" (read in the browser, so a server-rendered page stays valid). */\n  minDate?: Date | \"today\";\n  /** The last day that can be picked: a date, or \"today\" (a date of birth, a report period). */\n  maxDate?: Date | \"today\";\n  /** Dates that cannot be selected, or a predicate that identifies them. Defaults to no additional disabled dates. */\n  disabledDates?: CalendarDateMatcher;\n  /** Locale used for number, date or text formatting. Defaults to the runtime locale. */\n  locale?: string;\n  /** 0 Sunday, 1 Monday. Default 1. */\n  weekStartsOn?: 0 | 1;\n  /** Months side by side. Default 1. */\n  months?: 1 | 2;\n  /** Mark today with a dot. Default true. */\n  showToday?: boolean;\n  /** How many records a day holds, drawn as one to three dots (three and a plus past that) and said in the day's\n   *  name: a number (0 is a day recorded with none), or null for a day never recorded, drawn faint and hatched so\n   *  it doesn't read as an empty one. */\n  marks?: (date: Date) => number | null | undefined;\n  /** What `marks` counts, singular and plural, for the day's name (\"Tue 14 Oct, 2 workouts\"). */\n  markNoun?: [string, string];\n  /** Where it opens: the days, or the years for a date of birth or another far date. The month title switches\n   *  between them either way. Default \"days\". */\n  defaultView?: \"days\" | \"years\";\n  /** Controlled picker view; omitted by default to keep navigation local. */\n  view?: \"days\" | \"months\" | \"years\";\n  /** Reports navigation between day, month and year choices; no callback by default. */\n  onViewChange?: (view: \"days\" | \"months\" | \"years\") => void;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n}\nexport interface CalendarSingleProps extends Common {\n  /** Selects single-day selection. Defaults to single. */\n  mode?: \"single\";\n  /** Controlled day or date range; null clears the selection; omit to use defaultValue. */\n  value?: Date | null;\n  /** Initial selected day when value is omitted; defaults to no selection. */\n  defaultValue?: Date | null;\n  /** Legacy value-change callback; omitted by default. Prefer onValueChange.\n   *  @deprecated Use onValueChange; this value callback remains supported and is omitted by default. */\n  onChange?: (date: Date) => void;\n  /** The picked day: the name every Beamline field uses. */\n  onValueChange?: (date: Date) => void;\n}\nexport interface CalendarRangeProps extends Common {\n  /** Selects date-range selection. Required; pass range. */\n  mode: \"range\";\n  /** Controlled day or date range; null clears the selection; omit to use defaultValue. */\n  value?: DateRange | null;\n  /** Initial selected range when value is omitted; defaults to no selection. */\n  defaultValue?: DateRange | null;\n  /** Legacy value-change callback; omitted by default. Prefer onValueChange.\n   *  @deprecated Use onValueChange; this value callback remains supported and is omitted by default. */\n  onChange?: (range: DateRange) => void;\n  /** The picked range: the name every Beamline field uses. */\n  onValueChange?: (range: DateRange) => void;\n  /** While a range is half chosen: the span from the first pick to the day under the pointer or keyboard focus, and\n   *  null once it is closed or cancelled (a summary can count the days as they are swept). */\n  onPendingChange?: (span: DateRange | null) => void;\n}\nexport type CalendarProps = CalendarSingleProps | CalendarRangeProps;\n\n/**\n * Month grids for picking a day or a range. The grid is a real table: arrows move by day and week, Page Up/Down by\n * month (Shift by year), Home/End to the week's ends, T back to today, Enter picks. In range mode the second pick closes the range and\n * the span follows the pointer until it does. The month title opens the years, a year its months, a month its days:\n * far dates (a date of birth) take three picks instead of hundreds of pages.\n */\nexport function Calendar(props: CalendarProps) {\n  useDeprecatedProp(\"Calendar\", \"onChange\", \"onValueChange\", props.onChange !== undefined);\n  // today is known only in the browser: rendered on a server (a Next.js prerender) the calendar holds its place, empty,\n  // and the browser fills it in before the first paint\n  const today = useToday();\n  if (!today)\n    return (\n      <div\n        ref={props.ref}\n        data-slot=\"calendar\"\n        aria-busy\n        style={{ minWidth: 252 * (props.months ?? 1), minHeight: 300, ...props.style }}\n        className={cn(\"relative inline-grid\", props.classNames?.root, props.className)}\n      />\n    );\n  return <CalendarMonths {...props} today={today} />;\n}\n\nfunction CalendarMonths({ today, ...props }: CalendarProps & { today: Date }) {\n  const i18n = useMessages(\"calendar\", englishMessages);\n\n  const minDate = props.minDate === \"today\" ? today : props.minDate;\n  const maxDate = props.maxDate === \"today\" ? today : props.maxDate;\n  const {\n    disabledDates,\n    locale: localeProp,\n    weekStartsOn = 1,\n    months = 1,\n    showToday = true,\n    marks,\n    markNoun,\n    className,\n    classNames,\n    style,\n    ref,\n  } = props;\n  const locale = useLocale(localeProp);\n\n  const range = props.mode === \"range\";\n  // one hook for both modes; each mode reports through its own typed callbacks\n  const [value, setValue] = useControllable<Date | DateRange | null>(props.value, props.defaultValue ?? null);\n  // A mode change can leave the uncontrolled value in the previous mode's shape.\n  const selected = !range && value instanceof Date ? value : null;\n  const rangeValue = range && value !== null && !(value instanceof Date) ? value : null;\n  const [shown, setShownMonth] = useControllable(\n    props.month ? firstOfMonth(props.month) : undefined,\n    () => {\n      // with nothing chosen it opens on today, kept inside the dates that can be picked (a date of birth ends years ago)\n      let start = props.month ?? props.defaultMonth ?? selected ?? rangeValue?.start ?? today;\n      if (!props.month && !props.defaultMonth && !selected && !rangeValue) {\n        if (maxDate && start > maxDate) start = maxDate;\n        if (minDate && start < minDate) start = minDate;\n      }\n      return firstOfMonth(start);\n    },\n    props.onMonthChange,\n  );\n  const setShown = (m: Date) => setShownMonth(firstOfMonth(m));\n  const [focus, setFocus] = useState<Date>(() => {\n    const preferred = startOfDay(selected ?? rangeValue?.start ?? today);\n    const end = addMonths(shown, months);\n    return preferred >= shown && preferred < end ? preferred : shown;\n  });\n  const [anchor, setAnchor] = useState<Date | null>(null);\n  const [hover, setHover] = useState<Date | null>(null);\n  // which view shows: the days, or the years and months the month title opens\n  type View = \"days\" | \"months\" | \"years\";\n  const [view, setViewValue] = useControllable<View>(props.view, props.defaultView ?? \"days\", props.onViewChange);\n  const grid = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(grid, ref);\n  // the chosen day (single mode) is one solid that travels to the next day picked, across weeks and months\n  const [monthsRef, chosen] = useIndicator<HTMLDivElement>(\n    \"[data-slot=calendar-day][data-chosen]\",\n    `${selected?.getTime()}:${shown.getTime()}:${view}`,\n    undefined,\n    \"rect\",\n  );\n  const chosenRef = useContour<HTMLSpanElement>(chosen);\n  const moved = useRef(false);\n  // a page turn on the days (not a return from the years or months, which has its own fade): which way, and a parity\n  // that restarts the arrival (calendar.module.css .months[data-turn])\n  const [turn, setTurn] = useState({ n: 0, dir: 1, at: shown.getTime(), view });\n  if (turn.at !== shown.getTime() || turn.view !== view)\n    setTurn({\n      n: view === \"days\" && turn.view === \"days\" && turn.at !== shown.getTime() ? turn.n + 1 : turn.n,\n      dir: shown.getTime() > turn.at ? 1 : -1,\n      at: shown.getTime(),\n      view,\n    });\n\n  // the years and months: the title opens them, a pick narrows down, Escape returns to the days\n  const [pickYear, setPickYear] = useState(() => shown.getFullYear());\n  const chosenDate = selected ?? rangeValue?.start ?? null;\n  const thisYear = today.getFullYear();\n  const yearFrom = minDate ? minDate.getFullYear() : thisYear - 120;\n  const yearTo = maxDate ? maxDate.getFullYear() : thisYear + 30;\n  const defaultPickFocus =\n    view === \"months\"\n      ? chosenDate && chosenDate.getFullYear() === pickYear\n        ? chosenDate.getMonth()\n        : shown.getMonth()\n      : Math.min(yearTo, Math.max(yearFrom, (chosenDate ?? shown).getFullYear()));\n  const [pickerFocus, setPickerFocus] = useState(() => ({ view, value: defaultPickFocus }));\n  // A parent can switch views without openYears/openMonths. A year is never a month index.\n  const pickFocus = pickerFocus.view === view ? pickerFocus.value : defaultPickFocus;\n  const setPickFocus = (value: number, mode: View = view) => setPickerFocus({ view: mode, value });\n  const focusAfter = useRef<\"picker\" | \"days\" | null>(view !== \"days\" ? \"picker\" : null);\n  // the days fade back in only after the years or months, never on an ordinary open (frequent use: no motion)\n  const [swapped, setSwapped] = useState(false);\n  const setView = (next: View, focusTo: \"picker\" | \"days\") => {\n    focusAfter.current = focusTo;\n    setSwapped(true);\n    setViewValue(next);\n  };\n  const monthOff = (y: number, m: number) =>\n    (!!minDate && new Date(y, m + 1, 0) < startOfDay(minDate)) ||\n    (!!maxDate && new Date(y, m, 1) > startOfDay(maxDate));\n  const openYears = () => {\n    setPickFocus(Math.min(yearTo, Math.max(yearFrom, (chosenDate ?? shown).getFullYear())), \"years\");\n    setView(\"years\", \"picker\");\n  };\n  const openMonths = (y: number) => {\n    setPickYear(y);\n    setPickFocus(chosenDate && chosenDate.getFullYear() === y ? chosenDate.getMonth() : shown.getMonth(), \"months\");\n    setView(\"months\", \"picker\");\n  };\n  const toDays = (y: number, m: number) => {\n    const last = new Date(y, m + 1, 0).getDate();\n    const next = new Date(y, m, Math.min(focus.getDate(), last));\n    setShown(new Date(y, m, 1));\n    setFocus(next);\n    setView(\"days\", \"days\");\n  };\n  // focus moves in the same commit as the view: a frame later, the clicked year is already gone and focus sits on the\n  // page, so an Escape in that frame reached the popover as \"close\" instead of \"back to the days\"\n  useLayoutEffect(() => {\n    const to = focusAfter.current;\n    focusAfter.current = null;\n    if (!to) return;\n    const root = grid.current;\n    const target = root?.querySelector<HTMLElement>(\n      to === \"days\" ? \"[data-day][tabindex='0']\" : \"[data-pick][tabindex='0']\",\n    );\n    target?.focus({ preventScroll: true });\n    // the chosen year sits in the middle of its scrolling list, without moving the page\n    const list = target?.closest<HTMLElement>(\"[data-slot=calendar-picker]\");\n    if (target && list) list.scrollTop = target.offsetTop - list.clientHeight / 2 + target.offsetHeight / 2;\n  }, [view, pickYear]);\n  const onPickKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (e.key === \"Escape\") {\n      e.preventDefault();\n      e.stopPropagation();\n      setView(\"days\", \"days\");\n      return;\n    }\n    const cols = view === \"years\" ? 4 : 3;\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const step: Record<string, number> = {\n      ArrowLeft: rtl ? 1 : -1,\n      ArrowRight: rtl ? -1 : 1,\n      ArrowUp: -cols,\n      ArrowDown: cols,\n    };\n    const [lo, hi] = view === \"years\" ? [yearFrom, yearTo] : [0, 11];\n    let next: number | null = null;\n    if (step[e.key] !== undefined) next = pickFocus + step[e.key];\n    if (e.key === \"Home\") next = lo;\n    if (e.key === \"End\") next = hi;\n    if (next === null) return;\n    e.preventDefault();\n    next = Math.min(hi, Math.max(lo, next));\n    setPickFocus(next);\n    const box = e.currentTarget;\n    requestAnimationFrame(() => box.querySelector<HTMLElement>(`[data-pick=\"${next}\"]`)?.focus());\n  };\n\n  const off = (d: Date) =>\n    (minDate && d < startOfDay(minDate)) ||\n    (maxDate && d > startOfDay(maxDate)) ||\n    (Array.isArray(disabledDates) ? disabledDates.some((x) => sameDay(x, d)) : !!disabledDates?.(d));\n  const end = addMonths(shown, months);\n  const tabDay =\n    focus >= shown && focus < end && !off(focus)\n      ? focus\n      : Array.from({ length: Math.round((end.getTime() - shown.getTime()) / DAY) }, (_, i) => addDays(shown, i)).find(\n          (d) => !off(d),\n        );\n  const fmtMonth = dateFormatter(locale, { month: \"long\", year: \"numeric\" });\n  const fmtDay = dateFormatter(locale, { weekday: \"long\", day: \"numeric\", month: \"long\", year: \"numeric\" });\n  const weekdays = useMemo(\n    () =>\n      Array.from({ length: 7 }, (_, i) => {\n        const d = new Date(2024, 0, 7 + weekStartsOn + i);\n        return {\n          short: dateFormatter(locale, { weekday: \"narrow\" }).format(d),\n          long: dateFormatter(locale, { weekday: \"long\" }).format(d),\n        };\n      }),\n    [locale, weekStartsOn],\n  );\n\n  // keyboard focus that leaves the shown months turns the page\n  useEffect(() => {\n    const last = addMonths(shown, months - 1);\n    if (focus < shown || focus > new Date(last.getFullYear(), last.getMonth() + 1, 0)) setShown(focus);\n  }, [focus]); // eslint-disable-line react-hooks/exhaustive-deps -- follows focus only\n  // ...and the focused day takes focus in the commit that shows it (a frame later the page might not have turned yet,\n  // and the old month's button, gone, had handed focus to the page)\n  useLayoutEffect(() => {\n    if (!moved.current) return;\n    const el = grid.current?.querySelector<HTMLElement>(`[data-day=\"${focus.getTime()}\"]`);\n    if (!el) return;\n    moved.current = false;\n    if (document.activeElement !== el) el.focus({ preventScroll: true });\n  });\n\n  const pick = (d: Date) => {\n    if (off(d)) return;\n    setFocus(d);\n    if (!range) {\n      setValue(d);\n      (props as CalendarSingleProps).onChange?.(d);\n      (props as CalendarSingleProps).onValueChange?.(d);\n      return;\n    }\n    if (!anchor) {\n      setAnchor(d);\n      return;\n    }\n    const [start, end] = d < anchor ? [d, anchor] : [anchor, d];\n    setAnchor(null);\n    setHover(null);\n    setValue({ start, end });\n    (props as CalendarRangeProps).onChange?.({ start, end });\n    (props as CalendarRangeProps).onValueChange?.({ start, end });\n  };\n  /** The first day from `d` on (stepping `dir`, a year at most) that can be picked; null when there is none. */\n  const openDay = (d: Date, dir: 1 | -1) => {\n    for (let i = 0, x = d; i < 366; i++, x = addDays(x, dir)) if (!off(x)) return x;\n    return null;\n  };\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (!(e.target instanceof HTMLElement) || !e.target.matches(\"button[data-day]\")) return;\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const map: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(focus, rtl ? 1 : -1),\n      ArrowRight: () => addDays(focus, rtl ? -1 : 1),\n      ArrowUp: () => addDays(focus, -7),\n      ArrowDown: () => addDays(focus, 7),\n      PageUp: () => addMonths(focus, e.shiftKey ? -12 : -1),\n      PageDown: () => addMonths(focus, e.shiftKey ? 12 : 1),\n      Home: () => addDays(focus, -((focus.getDay() - weekStartsOn + 7) % 7)),\n      End: () => addDays(focus, 6 - ((focus.getDay() - weekStartsOn + 7) % 7)),\n    };\n    if (map[e.key]) {\n      e.preventDefault();\n      const raw = map[e.key]();\n      // a closed day (a weekend, past the limits) cannot hold focus: walk on to the next open one the way the key went\n      // (Home and End inward, toward where focus was), or stay\n      const toward = e.key === \"Home\" || e.key === \"End\" ? (raw < focus ? 1 : -1) : raw < focus ? -1 : 1;\n      const next = openDay(raw, toward);\n      if (!next) return;\n      moved.current = true;\n      setFocus(next);\n      if (anchor) setHover(next);\n      return;\n    }\n    if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault();\n      pick(focus);\n    }\n    // T: back to today from wherever the pages have gone (focus moves there; Enter picks it)\n    if (e.key === \"t\" || e.key === \"T\") {\n      if (e.metaKey || e.ctrlKey || e.altKey) return;\n      e.preventDefault();\n      const next = openDay(startOfDay(today), 1) ?? openDay(startOfDay(today), -1);\n      if (!next) return;\n      moved.current = true;\n      setFocus(next);\n      if (anchor) setHover(next);\n      return;\n    }\n    if (e.key === \"Escape\" && anchor) {\n      e.preventDefault();\n      e.stopPropagation();\n      setAnchor(null);\n      setHover(null);\n    }\n  };\n\n  const span = anchor\n    ? hover\n      ? hover < anchor\n        ? { start: hover, end: anchor }\n        : { start: anchor, end: hover }\n      : { start: anchor, end: anchor }\n    : rangeValue;\n  const inSpan = (d: Date) => !!span && d >= startOfDay(span.start) && d <= startOfDay(span.end);\n  // the half-chosen span, told to the caller as it changes (keyed by its two ends, so a render alone never reports)\n  const pendingKey = anchor && span ? `${span.start.getTime()}:${span.end.getTime()}` : \"\";\n  const onPending = range ? (props as CalendarRangeProps).onPendingChange : undefined;\n  useEffect(() => {\n    if (!onPending) return;\n    const [a, b] = pendingKey ? pendingKey.split(\":\").map(Number) : [];\n    onPending(pendingKey ? { start: new Date(a), end: new Date(b) } : null);\n  }, [pendingKey]); // eslint-disable-line react-hooks/exhaustive-deps -- reports changes of the span only\n  const month = (offset: number) => {\n    const first = addMonths(shown, offset);\n    const lead = (first.getDay() - weekStartsOn + 7) % 7;\n    const start = addDays(first, -lead);\n    const weeks = Math.ceil((lead + new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate()) / 7);\n    return (\n      <div\n        key={offset}\n        data-slot=\"calendar-month\"\n        className={cn(\"grid content-start gap-2\", swapped && styles.viewIn, classNames?.month)}\n      >\n        <div\n          data-slot=\"calendar-monthTitle\"\n          className={cn(\"flex h-8 items-center justify-center text-sm font-medium text-ink\", classNames?.monthTitle)}\n          aria-live=\"polite\"\n        >\n          {offset === 0 ? (\n            <button\n              type=\"button\"\n              data-slot=\"calendar-title\"\n              aria-expanded={false}\n              aria-label={i18n(\"chooseAnotherMonthOrYear\", { value: fmtMonth.format(first) })}\n              onClick={openYears}\n              className={cn(styles.title, classNames?.title)}\n            >\n              {fmtMonth.format(first)}\n              <ChevronDown aria-hidden />\n            </button>\n          ) : (\n            fmtMonth.format(first)\n          )}\n        </div>\n        <table\n          data-slot=\"calendar-grid\"\n          role=\"grid\"\n          aria-label={fmtMonth.format(first)}\n          className={cn(\"border-collapse\", classNames?.grid)}\n        >\n          <thead>\n            <tr>\n              {weekdays.map((w) => (\n                <th\n                  data-slot=\"calendar-weekday\"\n                  key={w.long}\n                  scope=\"col\"\n                  abbr={w.long}\n                  className={cn(styles.square, \"text-2xs font-normal text-ink-3\", classNames?.weekday)}\n                >\n                  {w.short}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {Array.from({ length: weeks }, (_, w) => (\n              <tr key={w}>\n                {Array.from({ length: 7 }, (_, i) => {\n                  const d = addDays(start, w * 7 + i);\n                  const outside = d.getMonth() !== first.getMonth();\n                  if (outside)\n                    return (\n                      <td data-slot=\"calendar-blank\" key={`b${i}`} className={cn(styles.square, classNames?.blank)} />\n                    );\n                  const isOn = range\n                    ? !!span && (sameDay(d, span.start) || sameDay(d, span.end))\n                    : sameDay(d, selected);\n                  const mid = range && inSpan(d) && !isOn;\n                  const edgeStart = range && span && sameDay(d, span.start) && !sameDay(span.start, span.end);\n                  const edgeEnd = range && span && sameDay(d, span.end) && !sameDay(span.start, span.end);\n                  const disabled = off(d);\n                  const mark = marks ? marks(d) : undefined;\n                  const recorded = !marks || (mark !== null && mark !== undefined);\n                  const dayName = !marks\n                    ? fmtDay.format(d)\n                    : !recorded\n                      ? i18n(\"dayNotRecorded\", { day: fmtDay.format(d) })\n                      : mark === 0\n                        ? i18n(\"dayNone\", { day: fmtDay.format(d) })\n                        : i18n(\"dayCount\", {\n                            day: fmtDay.format(d),\n                            count: mark!,\n                            noun: markNoun ? markNoun[mark === 1 ? 0 : 1] : i18n(mark === 1 ? \"record\" : \"records\"),\n                          });\n                  return (\n                    <td\n                      data-slot=\"calendar-cell\"\n                      // keyed by its date: a page turn never hands the focused button to another day\n                      key={d.getTime()}\n                      role=\"gridcell\"\n                      aria-selected={isOn || mid || undefined}\n                      className={cn(\n                        \"relative p-0\",\n                        (mid || edgeStart || edgeEnd) && \"bg-accent-tint\",\n                        edgeStart && \"rounded-s-control\",\n                        edgeEnd && \"rounded-e-control\",\n                        mid && i === 0 && \"rounded-s-control\",\n                        mid && i === 6 && \"rounded-e-control\",\n                        classNames?.cell,\n                      )}\n                    >\n                      <button\n                        data-slot=\"calendar-day\"\n                        type=\"button\"\n                        data-day={d.getTime()}\n                        data-chosen={isOn && !range ? \"\" : undefined}\n                        tabIndex={sameDay(d, tabDay) ? 0 : -1}\n                        disabled={disabled}\n                        aria-label={dayName}\n                        data-unrecorded={recorded ? undefined : \"\"}\n                        aria-current={sameDay(d, today) ? \"date\" : undefined}\n                        onClick={() => pick(d)}\n                        onPointerEnter={() => {\n                          if (anchor) setHover(d);\n                        }}\n                        onFocus={() => setFocus(d)}\n                        className={cn(\n                          styles.day,\n                          isOn ? (range ? cn(\"m-solid\", styles.on) : styles.on) : mid ? styles.between : styles.free,\n                          classNames?.day,\n                        )}\n                      >\n                        {d.getDate()}\n                        {!!mark && mark > 0 && (\n                          <span aria-hidden data-slot=\"calendar-marks\" className={styles.marks}>\n                            {Array.from({ length: Math.min(3, mark) }, (_, k) => (\n                              <span key={k} className={styles.markDot} />\n                            ))}\n                            {mark > 3 && <span className={styles.markMore}>+</span>}\n                          </span>\n                        )}\n                        {showToday && sameDay(d, today) && !(mark && mark > 0) && (\n                          <span\n                            data-slot=\"calendar-today\"\n                            aria-hidden\n                            className={cn(\n                              \"absolute bottom-1 size-1 rounded-full\",\n                              isOn ? \"bg-[var(--pui-accent-foreground)]\" : \"bg-accent\",\n                              classNames?.today,\n                            )}\n                          />\n                        )}\n                      </button>\n                    </td>\n                  );\n                })}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    );\n  };\n  const canBack = view === \"months\" ? pickYear > yearFrom : !minDate || addMonths(shown, -1) >= firstOfMonth(minDate);\n  const canFwd = view === \"months\" ? pickYear < yearTo : !maxDate || addMonths(shown, months) <= startOfDay(maxDate);\n  const fmtShortMonth = dateFormatter(locale, { month: \"short\" });\n  const fmtLongMonth = dateFormatter(locale, { month: \"long\", year: \"numeric\" });\n  const picker = view !== \"days\" && (\n    <div\n      data-slot=\"calendar-month\"\n      className={cn(\"grid content-start gap-2\", styles.viewIn, classNames?.month)}\n      style={{ width: `calc(var(--_cell) * ${7 * months} + ${(months - 1) * 24}px)` }}\n    >\n      <div\n        data-slot=\"calendar-monthTitle\"\n        className={cn(\"flex h-8 items-center justify-center text-sm font-medium text-ink\", classNames?.monthTitle)}\n        aria-live=\"polite\"\n      >\n        <button\n          type=\"button\"\n          data-slot=\"calendar-title\"\n          aria-expanded\n          aria-label={view === \"years\" ? i18n(\"backToTheDays\") : i18n(\"chooseAnotherYear\", { pickYear: pickYear })}\n          onClick={() => (view === \"years\" ? setView(\"days\", \"days\") : openYears())}\n          className={cn(styles.title, classNames?.title)}\n        >\n          {view === \"years\" ? fmtMonth.format(shown) : pickYear}\n          <ChevronDown aria-hidden />\n        </button>\n      </div>\n      <div\n        data-slot=\"calendar-picker\"\n        data-view={view}\n        role=\"group\"\n        aria-label={view === \"years\" ? i18n(\"years\") : i18n(\"monthsOf\", { pickYear: pickYear })}\n        onKeyDown={onPickKey}\n        className={cn(styles.picker, view === \"years\" ? styles.years : styles.monthGrid, classNames?.picker)}\n      >\n        {view === \"years\"\n          ? Array.from({ length: yearTo - yearFrom + 1 }, (_, i) => yearFrom + i).map((y) => {\n              const on = chosenDate?.getFullYear() === y;\n              return (\n                <button\n                  key={y}\n                  type=\"button\"\n                  data-pick={y}\n                  data-slot=\"calendar-pick\"\n                  tabIndex={y === pickFocus ? 0 : -1}\n                  aria-pressed={on}\n                  onClick={() => openMonths(y)}\n                  onFocus={() => setPickFocus(y)}\n                  className={cn(styles.pick, on && cn(\"m-solid\", styles.pickOn), classNames?.pick)}\n                >\n                  {y}\n                  {showToday && y === thisYear && <span aria-hidden className={styles.pickToday} />}\n                </button>\n              );\n            })\n          : Array.from({ length: 12 }, (_, m) => {\n              const on = chosenDate?.getFullYear() === pickYear && chosenDate.getMonth() === m;\n              return (\n                <button\n                  key={m}\n                  type=\"button\"\n                  data-pick={m}\n                  data-slot=\"calendar-pick\"\n                  tabIndex={m === pickFocus ? 0 : -1}\n                  disabled={monthOff(pickYear, m)}\n                  aria-pressed={on}\n                  aria-label={fmtLongMonth.format(new Date(pickYear, m, 1))}\n                  onClick={() => toDays(pickYear, m)}\n                  onFocus={() => setPickFocus(m)}\n                  className={cn(styles.pick, on && cn(\"m-solid\", styles.pickOn), classNames?.pick)}\n                >\n                  {fmtShortMonth.format(new Date(pickYear, m, 1))}\n                  {showToday && pickYear === thisYear && m === today.getMonth() && (\n                    <span aria-hidden className={styles.pickToday} />\n                  )}\n                </button>\n              );\n            })}\n      </div>\n    </div>\n  );\n  return (\n    <div\n      ref={attach}\n      style={style}\n      data-slot=\"calendar\"\n      onKeyDown={onKey}\n      onPointerLeave={() => setHover(null)}\n      className={cn(\"relative inline-grid\", classNames?.root, className)}\n    >\n      <div\n        data-slot=\"calendar-navigation\"\n        className={cn(styles.navigation, \"absolute inset-x-0 top-0 flex justify-between\", classNames?.navigation)}\n      >\n        {view !== \"years\" && (\n          <>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={view === \"months\" ? i18n(\"previousYear\") : i18n(\"previousMonth\")}\n              disabled={!canBack}\n              onClick={() => (view === \"months\" ? openMonths(pickYear - 1) : setShown(addMonths(shown, -1)))}\n            >\n              <ChevronLeft className=\"rtl:rotate-180\" />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={view === \"months\" ? i18n(\"nextYear\") : i18n(\"nextMonth\")}\n              disabled={!canFwd}\n              onClick={() => (view === \"months\" ? openMonths(pickYear + 1) : setShown(addMonths(shown, 1)))}\n            >\n              <ChevronRight className=\"rtl:rotate-180\" />\n            </Button>\n          </>\n        )}\n      </div>\n      {/* months sit side by side and wrap onto the next row when the container is too narrow for both */}\n      <div\n        ref={monthsRef}\n        data-slot=\"calendar-months\"\n        data-turn={turn.n ? (turn.n % 2 ? \"a\" : \"b\") : undefined}\n        style={{ \"--_dir\": turn.dir } as CSSProperties}\n        // at rest there is no turn: a grid mounted later (back from the years) must not replay the last one\n        onAnimationEnd={(e) => {\n          if (turn.n && e.animationName.includes(\"turn-\")) setTurn((t) => ({ ...t, n: 0 }));\n        }}\n        className={cn(\n          styles.months,\n          \"relative isolate flex flex-wrap justify-center gap-x-6 gap-y-4\",\n          classNames?.months,\n        )}\n      >\n        {view === \"days\" && chosen && (\n          <span ref={chosenRef} aria-hidden data-slot=\"calendar-chosen\" className={styles.chosen} />\n        )}\n        {view === \"days\" ? Array.from({ length: months }, (_, i) => month(i)) : picker}\n      </div>\n      {range && anchor && (\n        <p data-slot=\"calendar-announcement\" className={cn(\"sr-only\", classNames?.announcement)} aria-live=\"polite\">\n          {i18n(\"startChosenChooseTheEnd\", { value: fmtDay.format(anchor) })}\n        </p>\n      )}\n    </div>\n  );\n}\n\nexport { DAY };\nexport default Calendar;\n"},{"path":"beamline/components/calendar/messages.ts","type":"registry:file","target":"src/ui/beamline/components/calendar/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  chooseAnotherMonthOrYear: \"{value}: choose another month or year\",\n  backToTheDays: \"Back to the days\",\n  chooseAnotherYear: \"{pickYear}: choose another year\",\n  years: \"Years\",\n  monthsOf: \"Months of {pickYear}\",\n  previousYear: \"Previous year\",\n  previousMonth: \"Previous month\",\n  nextYear: \"Next year\",\n  nextMonth: \"Next month\",\n  startChosenChooseTheEnd: \"Start {value} chosen; choose the end.\",\n  dayCount: \"{day}, {count} {noun}\",\n  dayNone: \"{day}, none\",\n  dayNotRecorded: \"{day}, not recorded\",\n  record: \"record\",\n  records: \"records\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/defaults.ts","content":"/** Pure allowed presentation defaults for Spinner; no component or runtime imports. */\nexport interface SpinnerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/spinner/messages.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/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\",\n  done: \"Done\",\n  error: \"Failed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/spinner.module.css","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* With a delay the ring waits unseen and then fades in, so quick work never flashes it; the result (a check or a\n     cross) always shows at once. */\n  .root[data-delay] {\n    animation: appear var(--pui-fade-in) var(--pui-ease-default) var(--_delay) both;\n  }\n  @keyframes appear {\n    from {\n      opacity: 0;\n    }\n  }\n  /* The turn runs on the compositor (a CSS animation on the svg box) and costs no JS per frame. When the work ends the\n     arc closes into a full ring over the standard duration, so stopping the turn is invisible, and the check or cross\n     draws itself a beat later. All timing comes from the motion tokens; the loop period follows --pui-motion-scale. */\n  .svg[data-state=\"loading\"] {\n    animation: turn calc(0.8s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1)) linear infinite;\n  }\n  .track {\n    stroke-opacity: 0.18;\n  }\n  .arc {\n    stroke-dasharray: 0.3 1;\n    transition: stroke-dasharray var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .svg:not([data-state=\"loading\"]) .arc {\n    stroke-dasharray: 1 1;\n  }\n  .mark {\n    stroke-dasharray: 1 1;\n    animation: draw var(--pui-duration-standard) var(--pui-ease-enter) var(--pui-duration-fast) both;\n  }\n  @keyframes turn {\n    to {\n      transform: rotate(1turn);\n    }\n  }\n  @keyframes draw {\n    from {\n      stroke-dashoffset: 1;\n    }\n    to {\n      stroke-dashoffset: 0;\n    }\n  }\n  .svg[data-paused] {\n    animation-play-state: paused;\n  }\n  .svg[data-reduced],\n  .svg[data-reduced] .mark {\n    animation: none;\n  }\n  /* Reduction retains the loading/result shape without a decorative loop. */\n  @media (prefers-reduced-motion: reduce) {\n    .svg[data-state=\"loading\"],\n    .mark {\n      animation: none;\n    }\n  }\n}\n"},{"path":"beamline/components/spinner/spinner.tsx","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SpinnerDefaults } from \"./defaults\";\nexport type { SpinnerDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./spinner.module.css\";\n\nexport interface SpinnerClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the svg part; no override by default. */\n  svg?: string;\n  /** Classes for the track part; no override by default. */\n  track?: string;\n  /** Classes for the arc part; no override by default. */\n  arc?: string;\n  /** Classes for the mark part; no override by default. */\n  mark?: string;\n}\n\nexport interface SpinnerProps {\n  /** `success` draws a check out of the ring, `error` a cross; both stop spinning. */\n  state?: \"loading\" | \"success\" | \"error\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Accessible name while loading. */\n  label?: string;\n  /** ms to wait before it shows: work that ends sooner never flashes a spinner (about 300 is a good start). Default 0. */\n  delay?: number;\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<HTMLSpanElement>;\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?: SpinnerClassNames;\n}\n\n/** Icon box from the foundation icon steps (§14.2): sm 14, md 16, lg 20. */\nconst PX = { sm: \"var(--pui-icon-sm)\", md: \"var(--pui-icon-md)\", lg: \"var(--pui-icon-lg)\" } as const;\n\n/**\n * A ring that spins while work runs and closes into a drawn check (or cross) when it ends — the result is a shape as\n * well as a colour. With reduced motion the loading ring holds still.\n */\nexport function Spinner({\n  state = \"loading\",\n  size: sizeProp,\n  label: labelProp,\n  delay = 0,\n  className,\n  classNames,\n  style,\n  ref,\n}: SpinnerProps) {\n  const i18n = useMessages(\"spinner\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const scopeDefaults = useComponentDefaults<SpinnerDefaults>(\"spinner\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n\n  const box = PX[size];\n  const done = state !== \"loading\";\n  const tone = state === \"success\" ? \"text-green\" : state === \"error\" ? \"text-red\" : \"text-ink-2\";\n  return (\n    <span\n      ref={attach}\n      data-slot=\"spinner\"\n      data-state={state}\n      data-delay={delay > 0 && state === \"loading\" ? \"\" : undefined}\n      role={done ? \"img\" : \"status\"}\n      aria-label={state === \"success\" ? i18n(\"done\") : state === \"error\" ? i18n(\"error\") : label}\n      className={cn(styles.root, \"relative inline-grid shrink-0 place-items-center\", tone, classNames?.root, className)}\n      style={{ width: box, height: box, \"--_delay\": `${delay}ms`, ...style } as CSSProperties}\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        width=\"100%\"\n        height=\"100%\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n        data-state={state}\n        data-slot=\"spinner-svg\"\n        data-reduced={reduced ? \"\" : undefined}\n        data-paused={active ? undefined : \"\"}\n        className={cn(styles.svg, classNames?.svg)}\n      >\n        <circle\n          data-slot=\"spinner-track\"\n          className={cn(styles.track, classNames?.track)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n        />\n        <circle\n          data-slot=\"spinner-arc\"\n          className={cn(styles.arc, classNames?.arc)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          pathLength={1}\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          transform=\"rotate(-90 12 12)\"\n        />\n        {state === \"success\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M7.5 12.5l3 3 6-6.5\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        )}\n        {state === \"error\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M8.5 8.5l7 7M15.5 8.5l-7 7\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n          />\n        )}\n      </svg>\n    </span>\n  );\n}\n\nexport default Spinner;\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-contour.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-contour.ts","content":"\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef } from \"react\";\nimport { createContour, type Contour } from \"../lib/contour\";\nimport type { IndicatorBox } from \"./use-indicator\";\n\n/**\n * Puts a decorative surface (a segmented thumb, a tab's pill or line) on the contour engine and moves it to `box`\n * whenever the box changes: the first placement is instant, later ones travel as matter (ui/lib/contour.ts). Returns a\n * ref for the surface. The surface stays aria-hidden; the control's own state has already changed by the time it moves.\n */\nexport function useContour<T extends HTMLElement>(box: IndicatorBox | null) {\n  const contour = useRef<Contour | null>(null);\n  const ref = useCallback((el: T | null) => {\n    contour.current?.destroy();\n    contour.current = el ? createContour(el) : null;\n  }, []);\n  useLayoutEffect(() => {\n    // a choice travels; the first placement and a reflow of the same choice are simply there\n    if (box) contour.current?.to(box, { instant: !box.ready || box.cause === \"layout\" });\n  }, [box]);\n  return ref;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\n}\n"},{"path":"beamline/hooks/use-indicator.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-indicator.ts","content":"\"use client\";\n\nimport { APPEARANCE_EVENT } from \"../lib/motion-tokens\";\nimport { useLayoutEffect, useRef, useState, type CSSProperties, type RefObject } from \"react\";\n\nexport interface IndicatorBox {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  /** False on the first placement, so the indicator appears in place instead of sliding in from the corner. */\n  ready: boolean;\n  /** Which way the last move went on each axis (-1 left or up, 1 right or down, 0 still), for the edge that leads. */\n  dx: -1 | 0 | 1;\n  dy: -1 | 0 | 1;\n  /** choice: a selection changed; layout: the same choice moved because the page reflowed (a resize, a font, a\n   *  type scale). A surface may travel for a choice and should simply be there after a reflow. */\n  cause: \"choice\" | \"layout\";\n}\n\ntype Box = Pick<IndicatorBox, \"x\" | \"y\" | \"w\" | \"h\">;\nconst same = (a: IndicatorBox | null, b: Box) => a !== null && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;\n\n/**\n * Follows the chosen item inside a container: the thumb of a segmented control, the pill or line of tabs, the ring of\n * radio cards. Returns a ref for the container and the box of the first element matching `selector` in it, relative\n * to the container (null when nothing matches). It measures before paint whenever `key` changes, and again when the\n * container resizes or a selection attribute changes inside it (for parts whose choice lives in a library, like tabs).\n */\nexport function useIndicator<T extends HTMLElement>(\n  selector: string,\n  key?: unknown,\n  onPlace?: (item: HTMLElement, container: T) => void,\n  /** offset (default): the item's offset box, for items whose offset parent is the container. rect: measured on screen\n   *  relative to the container, for items nested in positioned cells (a calendar day in its table cell). */\n  measure: \"offset\" | \"rect\" = \"offset\",\n): [RefObject<T | null>, IndicatorBox | null] {\n  const ref = useRef<T>(null);\n  const [box, setBox] = useState<IndicatorBox | null>(null);\n  const placed = useRef(onPlace);\n  useLayoutEffect(() => {\n    placed.current = onPlace;\n  });\n  useLayoutEffect(() => {\n    const container = ref.current;\n    if (!container) return;\n    const place = (cause: IndicatorBox[\"cause\"] = \"choice\") => {\n      const item = container.querySelector<HTMLElement>(selector);\n      if (!item) {\n        setBox(null);\n        return;\n      }\n      let next = { x: item.offsetLeft, y: item.offsetTop, w: item.offsetWidth, h: item.offsetHeight };\n      if (measure === \"rect\") {\n        const a = item.getBoundingClientRect();\n        const c = container.getBoundingClientRect();\n        next = {\n          x: Math.round(a.left - c.left - container.clientLeft + container.scrollLeft),\n          y: Math.round(a.top - c.top - container.clientTop + container.scrollTop),\n          w: item.offsetWidth,\n          h: item.offsetHeight,\n        };\n      }\n      setBox((b) =>\n        same(b, next)\n          ? b\n          : {\n              ...next,\n              cause,\n              ready: b !== null,\n              dx: b ? (Math.sign(next.x - b.x) as IndicatorBox[\"dx\"]) : 0,\n              dy: b ? (Math.sign(next.y - b.y) as IndicatorBox[\"dy\"]) : 0,\n            },\n      );\n      placed.current?.(item, container);\n    };\n    const resizes = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(() => place(\"layout\"));\n    resizes?.observe(container);\n    const observed = new Set<Element>();\n    const observeItems = () => {\n      // A full-width fixed-height list can stay the same size while its tabs/radio choices grow with type or labels.\n      // Ignore the decorative indicator itself, whose own dimensions are the output of this measurement.\n      const items = new Set([...container.children].filter((child) => child.getAttribute(\"aria-hidden\") !== \"true\"));\n      for (const old of observed)\n        if (!items.has(old)) {\n          resizes?.unobserve(old);\n          observed.delete(old);\n        }\n      for (const item of items)\n        if (!observed.has(item)) {\n          resizes?.observe(item);\n          observed.add(item);\n        }\n    };\n    observeItems();\n    place();\n    const mutations = new MutationObserver(() => {\n      observeItems();\n      place(\"choice\");\n    });\n    mutations.observe(container, {\n      subtree: true,\n      childList: true,\n      attributes: true,\n      attributeFilter: [\"data-state\", \"aria-checked\", \"aria-selected\", \"aria-current\", \"data-active\", \"data-open\"],\n    });\n    const reflow = () => place(\"layout\");\n    container.ownerDocument.addEventListener(APPEARANCE_EVENT, reflow);\n    container.ownerDocument.fonts?.addEventListener(\"loadingdone\", reflow);\n    return () => {\n      mutations.disconnect();\n      resizes?.disconnect();\n      container.ownerDocument.removeEventListener(APPEARANCE_EVENT, reflow);\n      container.ownerDocument.fonts?.removeEventListener(\"loadingdone\", reflow);\n    };\n  }, [selector, key, measure]);\n  return [ref, box];\n}\n\n/** The box as custom properties, for a stylesheet to place the indicator with: --pui-indicator-x/-y/-w/-h, and its\n *  four edges --pui-glide-l/-r/-t/-b, which the `glide` class moves one at a time. */\nexport function indicatorStyle(box: IndicatorBox): CSSProperties {\n  return {\n    \"--pui-indicator-x\": `${box.x}px`,\n    \"--pui-indicator-y\": `${box.y}px`,\n    \"--pui-indicator-w\": `${box.w}px`,\n    \"--pui-indicator-h\": `${box.h}px`,\n    \"--pui-glide-l\": `${box.x}px`,\n    \"--pui-glide-r\": `${box.x + box.w}px`,\n    \"--pui-glide-t\": `${box.y}px`,\n    \"--pui-glide-b\": `${box.y + box.h}px`,\n  } as CSSProperties;\n}\n\nconst AXIS = { x: [\"left\", undefined, \"right\"], y: [\"up\", undefined, \"down\"] } as const;\n\n/** Everything an indicator wearing the `glide` class needs (utilities.css): its place, whether it may move yet, and\n *  which way it is going, so the edge on that side leads and the other follows: it stretches toward the new choice and\n *  gathers there. */\nexport function glideProps(box: IndicatorBox) {\n  return {\n    style: indicatorStyle(box),\n    \"data-ready\": box.ready ? \"\" : undefined,\n    \"data-glide-x\": AXIS.x[box.dx + 1],\n    \"data-glide-y\": AXIS.y[box.dy + 1],\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/contour.ts","type":"registry:file","target":"src/ui/beamline/lib/contour.ts","content":"import { readMotionFactor } from \"./motion-tokens\";\n\n/**\n * Contour: a surface whose outline moves like matter instead of a box that resizes. Beamline's own model (research/\n * morphing-interfaces-2026-10-04): a rounded quadrilateral whose four corners each follow their target on a critically\n * damped spring. The corner facing the way it travels answers fastest and the far one slowest, so it leads and gathers;\n * each edge's midpoint heads for where its edge will be a little ahead of its corners and may only bulge outward (the\n * leading edge rounds out like the front of a drop, the tail stays smooth), and while the shape is longer than its target the long edges\n * pinch toward the centre, so a fast move draws a neck; while it is stretched its corners round toward a capsule, as a moving drop does. No spring overshoots: the elasticity is the parts arriving at\n * different moments, never bounce. At rest the surface is its plain CSS box with no clip and no frames.\n *\n * The look decides the material through four inputs on the element (preset-source.css): --m-morph-spread (how unevenly\n * the corners answer; 0 is rigid travel), --m-morph-front (px the leading edge may round out), --m-morph-waist (how much\n * a stretch pinches) and --m-morph-weight (a multiplier on the response time).\n */\nexport interface ContourBox {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\nexport interface Contour {\n  /** Move the surface to `box` (in its offset parent's coordinates). `instant` places it with no motion. */\n  to(box: ContourBox, options?: { instant?: boolean }): void;\n  /** Stop and leave the surface at its target. */\n  destroy(): void;\n}\n\ntype Point = [number, number];\ntype Corner = \"tl\" | \"tr\" | \"br\" | \"bl\";\ntype Edge = \"t\" | \"r\" | \"b\" | \"l\";\nconst CORNERS: Corner[] = [\"tl\", \"tr\", \"br\", \"bl\"];\nconst EDGES: Edge[] = [\"t\", \"r\", \"b\", \"l\"];\n/** Outward direction of each corner, for deciding which one leads. */\nconst OUT: Record<Corner, Point> = { tl: [-1, -1], tr: [1, -1], br: [1, 1], bl: [-1, 1] };\n/** Outward direction of each edge. */\nconst NORMAL: Record<Edge, Point> = { t: [0, -1], r: [1, 0], b: [0, 1], l: [-1, 0] };\n/** Settles at the motion-token morph spring at the foundation default. */\nconst BASE_RESPONSE = 0.26;\n\ninterface Mechanics {\n  spread: number;\n  front: number;\n  waist: number;\n  weight: number;\n  /** The colour of the edge drawn along a non-rectangular outline (the look's hairline, which a clip would cut off). */\n  rim: string;\n  rimWidth: number;\n}\n\n/** One step of a critically damped spring with response time `t` (s), in closed form so a long frame cannot explode. */\nfunction step(x: number, v: number, target: number, t: number, dt: number): [number, number] {\n  const w = (2 * Math.PI) / (t * 1.2);\n  const d = x - target;\n  const e = Math.exp(-w * dt);\n  return [target + (d + (v + w * d) * dt) * e, (v - w * (v + w * d) * dt) * e];\n}\n\nconst cornersOf = (b: ContourBox): Record<Corner, Point> => ({\n  tl: [b.x, b.y],\n  tr: [b.x + b.w, b.y],\n  br: [b.x + b.w, b.y + b.h],\n  bl: [b.x, b.y + b.h],\n});\nconst midsOf = (c: Record<Corner, Point>): Record<Edge, Point> => ({\n  t: [(c.tl[0] + c.tr[0]) / 2, (c.tl[1] + c.tr[1]) / 2],\n  r: [(c.tr[0] + c.br[0]) / 2, (c.tr[1] + c.br[1]) / 2],\n  b: [(c.br[0] + c.bl[0]) / 2, (c.br[1] + c.bl[1]) / 2],\n  l: [(c.bl[0] + c.tl[0]) / 2, (c.bl[1] + c.tl[1]) / 2],\n});\nconst zero = <K extends string>(keys: K[]) => Object.fromEntries(keys.map((k) => [k, [0, 0]])) as Record<K, Point>;\nconst num = (style: CSSStyleDeclaration, name: string, fallback: number) => {\n  const value = Number.parseFloat(style.getPropertyValue(name));\n  return Number.isFinite(value) ? value : fallback;\n};\n\n/** All moving contours share one frame loop, which stops when the last one settles. */\nconst moving = new Set<(dt: number) => boolean>();\nlet frame = 0;\nlet last = 0;\nfunction tick(now: number) {\n  const dt = Math.min(0.032, Math.max(0.001, (now - last) / 1000));\n  last = now;\n  for (const advance of moving) if (!advance(dt)) moving.delete(advance);\n  frame = moving.size ? requestAnimationFrame(tick) : 0;\n}\nfunction run(advance: (dt: number) => boolean) {\n  moving.add(advance);\n  if (!frame) {\n    last = performance.now();\n    frame = requestAnimationFrame(tick);\n  }\n}\n\n/** One contour per element, so a remount (or React's development double mount) keeps its place and keeps moving. */\nconst contours = new WeakMap<HTMLElement, Contour>();\nexport function createContour(el: HTMLElement): Contour {\n  const existing = contours.get(el);\n  if (existing) return existing;\n  let target: ContourBox | null = null;\n  let pos: Record<Corner, Point> | null = null;\n  let vel = zero(CORNERS);\n  let mid: Record<Edge, Point> = { t: [0, 0], r: [0, 0], b: [0, 0], l: [0, 0] };\n  let midVel = zero(EDGES);\n  let mech: Mechanics = { spread: 0.35, front: 5, waist: 0.16, weight: 1, rim: \"none\", rimWidth: 1 };\n  let rimPath: SVGPathElement | null = null;\n  const rim = (d: string | null, w = 0, h = 0) => {\n    if (!d || mech.rim === \"none\") {\n      if (rimPath) (rimPath.ownerSVGElement as SVGSVGElement).style.display = \"none\";\n      return;\n    }\n    if (!rimPath) {\n      const ns = \"http://www.w3.org/2000/svg\";\n      const svg = el.ownerDocument.createElementNS(ns, \"svg\");\n      svg.setAttribute(\"aria-hidden\", \"true\");\n      svg.setAttribute(\"data-contour-rim\", \"\");\n      svg.style.cssText = \"position:absolute;left:0;top:0;z-index:2;overflow:visible;pointer-events:none\";\n      rimPath = el.ownerDocument.createElementNS(ns, \"path\");\n      rimPath.setAttribute(\"fill\", \"none\");\n      svg.appendChild(rimPath);\n      el.appendChild(svg);\n    }\n    const svg = rimPath.ownerSVGElement as SVGSVGElement;\n    svg.style.display = \"\";\n    svg.setAttribute(\"width\", String(w));\n    svg.setAttribute(\"height\", String(h));\n    rimPath.setAttribute(\"d\", d);\n    // the clip keeps the inner half of the stroke: an edge of rimWidth inside the outline\n    rimPath.setAttribute(\"stroke-width\", String(mech.rimWidth * 2));\n    rimPath.style.stroke = mech.rim;\n  };\n  let response = BASE_RESPONSE;\n  let radius = 0;\n  let running = false;\n\n  const rest = () => {\n    if (!target) return;\n    el.style.transform = `translate(${target.x}px, ${target.y}px)`;\n    el.style.width = `${target.w}px`;\n    el.style.height = `${target.h}px`;\n    el.style.clipPath = \"\";\n    rim(null);\n    delete el.dataset.morphing;\n  };\n\n  const draw = () => {\n    if (!pos || !target) return;\n    const c = pos;\n    const w = (c.tr[0] - c.tl[0] + c.br[0] - c.bl[0]) / 2;\n    const h = (c.bl[1] - c.tl[1] + c.br[1] - c.tr[1]) / 2;\n    // the long edges pinch while the shape is longer than its target (area kept, roughly)\n    const pinchY = Math.min(h * mech.waist * 0.5, 4, h * Math.max(0, w / target.w - 1) * mech.waist);\n    const pinchX = Math.min(w * mech.waist * 0.5, 4, w * Math.max(0, h / target.h - 1) * mech.waist);\n    const edges: Record<Edge, Point> = {\n      t: [mid.t[0], mid.t[1] + pinchY],\n      b: [mid.b[0], mid.b[1] - pinchY],\n      l: [mid.l[0] + pinchX, mid.l[1]],\n      r: [mid.r[0] - pinchX, mid.r[1]],\n    };\n    // still a rectangle (a rigid look, or a straight move): move the box itself and keep every bit of the look's paint\n    const flat = (a: number, b: number) => Math.abs(a - b) < 0.35;\n    const corners = midsOf(c);\n    if (\n      flat(c.tl[1], c.tr[1]) &&\n      flat(c.bl[1], c.br[1]) &&\n      flat(c.tl[0], c.bl[0]) &&\n      flat(c.tr[0], c.br[0]) &&\n      EDGES.every((k) => flat(edges[k][0], corners[k][0]) && flat(edges[k][1], corners[k][1]))\n    ) {\n      el.style.transform = `translate(${c.tl[0]}px, ${c.tl[1]}px)`;\n      el.style.width = `${c.tr[0] - c.tl[0]}px`;\n      el.style.height = `${c.bl[1] - c.tl[1]}px`;\n      el.style.clipPath = \"\";\n      rim(null);\n      return;\n    }\n    const all = [...CORNERS.map((k) => c[k]), ...EDGES.map((k) => edges[k])];\n    const minX = Math.floor(Math.min(...all.map((p) => p[0]))) - 1;\n    const minY = Math.floor(Math.min(...all.map((p) => p[1]))) - 1;\n    const maxX = Math.ceil(Math.max(...all.map((p) => p[0]))) + 1;\n    const maxY = Math.ceil(Math.max(...all.map((p) => p[1]))) + 1;\n    const local = (p: Point): Point => [p[0] - minX, p[1] - minY];\n    const C = CORNERS.map((k) => local(c[k]));\n    const E = ([\"t\", \"r\", \"b\", \"l\"] as Edge[]).map((k) => local(edges[k]));\n    const len = (a: Point, b: Point) => Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;\n    const toward = (a: Point, b: Point, d: number): Point => {\n      const l = len(a, b);\n      return [a[0] + ((b[0] - a[0]) / l) * d, a[1] + ((b[1] - a[1]) / l) * d];\n    };\n    // each corner backs off its radius along both of its real edges and turns through the corner itself, so a sheared\n    // shape never folds over; each edge bows through its midpoint\n    // a moving drop is round: corners grow toward a capsule while the shape is stretched or far from home\n    const away = Math.max(w / target.w - 1, h / target.h - 1, 0);\n    const round = mech.front > 0 ? Math.min(1, away * 2.5) : 0;\n    const r = radius + (Math.min(w, h) / 2 - radius) * Math.max(0, round);\n    const rr = C.map((p, i) => Math.min(r, len(p, C[(i + 1) % 4]) / 2.2, len(p, C[(i + 3) % 4]) / 2.2));\n    const into = C.map((p, i) => toward(p, C[(i + 3) % 4], rr[i]));\n    const out = C.map((p, i) => toward(p, C[(i + 1) % 4], rr[i]));\n    const through = (a: Point, m: Point, b: Point): Point => [\n      2 * m[0] - (a[0] + b[0]) / 2,\n      2 * m[1] - (a[1] + b[1]) / 2,\n    ];\n    const f = (p: Point) => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`;\n    let d = `M${f(out[0])}`;\n    for (let i = 0; i < 4; i++) {\n      const j = (i + 1) % 4;\n      // an edge the rounded corners have nearly used up can only bow a little, or its middle would point\n      const a = out[i];\n      const b = into[j];\n      const chord: Point = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];\n      const bow: Point = [E[i][0] - chord[0], E[i][1] - chord[1]];\n      const most = len(a, b) * 0.18;\n      const bowLen = Math.hypot(bow[0], bow[1]);\n      const m: Point =\n        bowLen > most ? [chord[0] + (bow[0] / bowLen) * most, chord[1] + (bow[1] / bowLen) * most] : E[i];\n      d += ` Q${f(through(a, m, b))} ${f(b)} Q${f(C[j])} ${f(out[j])}`;\n    }\n    el.style.transform = `translate(${minX}px, ${minY}px)`;\n    el.style.width = `${maxX - minX}px`;\n    el.style.height = `${maxY - minY}px`;\n    el.style.clipPath = `path(\"${d}Z\")`;\n    rim(`${d}Z`, maxX - minX, maxY - minY);\n  };\n\n  const advance = (dt: number) => {\n    if (!pos || !target || !el.isConnected) return (running = false);\n    const goal = cornersOf(target);\n    let busy = false;\n    for (const k of CORNERS) {\n      const p = pos[k];\n      const v = vel[k];\n      const g = goal[k];\n      const dx = g[0] - p[0];\n      const dy = g[1] - p[1];\n      const dist = Math.hypot(dx, dy);\n      // -1 … 1: how squarely this corner faces the way it is going\n      const along = dist > 0.5 ? (OUT[k][0] * dx + OUT[k][1] * dy) / (dist * Math.SQRT2) : 0;\n      const t = response * (1 + mech.spread * 0.4 - mech.spread * along);\n      for (const a of [0, 1] as const) {\n        [p[a], v[a]] = step(p[a], v[a], g[a], t, dt);\n        if (Math.abs(p[a] - g[a]) > 0.2 || Math.abs(v[a]) > 4) busy = true;\n      }\n    }\n    const now = midsOf(pos);\n    const aim = midsOf(goal);\n    for (const k of EDGES) {\n      for (const a of [0, 1] as const)\n        [mid[k][a], midVel[k][a]] = step(mid[k][a], midVel[k][a], aim[k][a], response * 0.75, dt);\n      // the lead is a few pixels at most, and never more than a fraction of the edge or of the shape's depth\n      const across = k === \"t\" || k === \"b\" ? target.h : target.w;\n      const edge =\n        k === \"t\" ? [pos.tl, pos.tr] : k === \"b\" ? [pos.br, pos.bl] : k === \"l\" ? [pos.bl, pos.tl] : [pos.tr, pos.br];\n      const limit = Math.min(\n        mech.front,\n        across * 0.3,\n        Math.hypot(edge[1][0] - edge[0][0], edge[1][1] - edge[0][1]) * 0.15,\n      );\n      // edges only ever bulge outward: the front rounds out, the tail stays smooth instead of curling in\n      let ox = mid[k][0] - now[k][0];\n      let oy = mid[k][1] - now[k][1];\n      const [nx, ny] = NORMAL[k];\n      const inward = ox * nx + oy * ny;\n      if (inward < 0) {\n        ox -= inward * nx;\n        oy -= inward * ny;\n        mid[k][0] = now[k][0] + ox;\n        mid[k][1] = now[k][1] + oy;\n      }\n      const off = Math.hypot(ox, oy);\n      if (off > limit) {\n        mid[k][0] = now[k][0] + (ox / off) * limit;\n        mid[k][1] = now[k][1] + (oy / off) * limit;\n      }\n      if (Math.abs(mid[k][0] - aim[k][0]) > 0.2 || Math.abs(mid[k][1] - aim[k][1]) > 0.2) busy = true;\n    }\n    if (!busy) {\n      pos = goal;\n      vel = zero(CORNERS);\n      mid = midsOf(goal);\n      midVel = zero(EDGES);\n      rest();\n      return (running = false);\n    }\n    draw();\n    return true;\n  };\n\n  const contour: Contour = {\n    to(box, options) {\n      const same = target && target.x === box.x && target.y === box.y && target.w === box.w && target.h === box.h;\n      if (same && !options?.instant) return;\n      target = { ...box };\n      const style = getComputedStyle(el);\n      const r = style.borderTopLeftRadius;\n      radius = r.endsWith(\"%\") ? (Number.parseFloat(r) / 100) * Math.min(box.w, box.h) : Number.parseFloat(r) || 0;\n      mech = {\n        spread: num(style, \"--m-morph-spread\", 0.35),\n        front: num(style, \"--m-morph-front\", 6),\n        waist: num(style, \"--m-morph-waist\", 0.18),\n        weight: num(style, \"--m-morph-weight\", 1),\n        rim: style.getPropertyValue(\"--m-morph-rim\").trim() || \"none\",\n        rimWidth: num(style, \"--m-morph-rim-width\", 1),\n      };\n      const factor = readMotionFactor(el);\n      response = BASE_RESPONSE * mech.weight * factor;\n      // first placement, reduced motion, or a hidden surface: be there, with nothing to watch\n      if (!pos || options?.instant || factor < 0.05 || !el.getClientRects().length) {\n        pos = cornersOf(target);\n        vel = zero(CORNERS);\n        mid = midsOf(pos);\n        midVel = zero(EDGES);\n        moving.delete(advance);\n        running = false;\n        rest();\n        return;\n      }\n      el.dataset.morphing = \"\";\n      if (!running) {\n        running = true;\n        run(advance);\n      }\n    },\n    destroy() {\n      moving.delete(advance);\n      running = false;\n      if (target) rest();\n    },\n  };\n  contours.set(el, contour);\n  return contour;\n}\n"},{"path":"beamline/lib/now.ts","type":"registry:file","target":"src/ui/beamline/lib/now.ts","content":"import { useContext, useLayoutEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { ActivityContext } from \"../runtime/activity\";\n\n/*\n * The time, read only in the browser. A component that reads the clock while it renders breaks server rendering:\n * Next.js refuses to prerender a page that does (\"encountered the unstable value `new Date()` in a Client Component\",\n * create-next-app's default since 16), and any server's time differs from the browser's at hydration. These hooks give\n * null on the server and while hydrating, and the real value in the browser before the first paint.\n */\n\nconst still = () => () => {};\nconst startOfToday = () => {\n  const d = new Date();\n  return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n};\nconst onServer = () => null;\n\n/** Today at 00:00 local time; null on the server and while hydrating. */\nexport function useToday(): Date | null {\n  const day = useSyncExternalStore(still, startOfToday, onServer);\n  return useMemo(() => (day === null ? null : new Date(day)), [day]);\n}\n\n/** The time in ms, read before the first paint and again every `every` ms while `ticking`; null on the server. */\nexport function useNow(ticking = false, every = 1000, until?: number): number | null {\n  const active = useContext(ActivityContext);\n  const [now, setNow] = useState<number | null>(null);\n  // A finite deadline must reach its completion callback even when the host is idle, then stop ticking.\n  const running = ticking && (until === undefined ? active : now === null || now < until);\n  useLayoutEffect(() => {\n    setNow(Date.now());\n    if (!running) return;\n    const id = setInterval(() => setNow(Date.now()), every);\n    return () => clearInterval(id);\n  }, [running, every, until]);\n  return now;\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":"Calendar is in src/ui/beamline/components/calendar/calendar.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/calendar"}}