{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion","title":"Motion","description":"Animate items that appear, leave, move or replace one another, and numbers that change, with no animation code. Search terms: presence, animate presence, transition, list, reorder, reveal, swap, enter, exit, rolling number, odometer, counter, tween, chart.","dependencies":["@base-ui/react@1.7.0","motion@12.43.0"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/motion/motion.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n  AnimatePresence,\n  MotionConfig,\n  PresenceContext,\n  useIsPresent,\n  usePresence,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport { useSiteplaneNonce } from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { useHeightFollower } from \"./motion-follow\";\nimport {\n  GlideOnChange,\n  leavingProps,\n  measureSwapTiming,\n  swapPresence,\n  toMotionElement,\n  useLeavingEntries,\n  usePresenceMotion,\n  type MotionLayout,\n  type SwapTiming,\n} from \"./motion-presence\";\n\nexport type { MotionLayout };\n// Components whose panes stay mounted while they leave (Tabs) take their pin\n// and height follow from useLeavingPane (motion-follow) and their switch rule\n// from panelSwitchClassName (motion-presence). Both are kit internals, imported\n// by file path, not part of the public Motion API.\n\ntype NativeProps<Tag extends \"div\" | \"li\" | \"ul\"> = Omit<\n  React.ComponentPropsWithRef<Tag>,\n  // Motion owns these handlers; React's versions have a different signature.\n  \"onAnimationStart\" | \"onDrag\" | \"onDragEnd\" | \"onDragStart\"\n>;\n\ntype MotionEntryValue = {\n  /** The entry holds a MotionItem, which plays the exit. */\n  claim: () => void;\n  /** Whether no other item follows, so no space is needed after it. */\n  last: boolean;\n  /** The item has left the list and plays its exit. */\n  leaving: boolean;\n  /** The exit is over: drop the entry. */\n  remove: () => void;\n};\n\nconst MotionEntryContext = React.createContext<MotionEntryValue | null>(null);\n\n/**\n * Presence that takes a leaving child out of the flow while it plays its exit.\n * Motion pins that child with a `<style>` element; under a Content Security\n * Policy with nonces it carries the provider's nonce. MotionConfig merges with\n * any MotionConfig of the application around it, so its transition and\n * reduced-motion settings still apply.\n */\nfunction PopLayoutPresence({\n  children,\n}: {\n  children: React.ReactNode;\n}): React.ReactElement {\n  const nonce = useSiteplaneNonce();\n  const presence = (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      {children}\n    </AnimatePresence>\n  );\n  return nonce ? (\n    <MotionConfig nonce={nonce}>{presence}</MotionConfig>\n  ) : (\n    presence\n  );\n}\n\n/**\n * What a stack item knows about its place in the list. A leaving item stays in\n * the list until its exit has played, right after the item it followed.\n */\nfunction MotionEntry({\n  children,\n  entryKey,\n  last,\n  leaving,\n  onGone,\n}: {\n  children: React.ReactNode;\n  entryKey: React.Key;\n  last: boolean;\n  leaving: boolean;\n  onGone: (key: React.Key) => void;\n}): React.ReactElement {\n  const claimed = React.useRef(false);\n  const value = React.useMemo<MotionEntryValue>(\n    () => ({\n      claim: () => {\n        claimed.current = true;\n      },\n      last,\n      leaving,\n      remove: () => onGone(entryKey),\n    }),\n    [entryKey, last, leaving, onGone],\n  );\n  // Without a MotionItem there is no exit to play: the entry goes at once.\n  React.useLayoutEffect(() => {\n    if (leaving && !claimed.current) onGone(entryKey);\n  }, [entryKey, leaving, onGone]);\n  return (\n    <MotionEntryContext.Provider value={value}>\n      {children}\n    </MotionEntryContext.Provider>\n  );\n}\n\ntype MotionListContextValue = {\n  gap: boolean;\n  layout: MotionLayout;\n  /** True from the list's first frame: items that mount later animate in. */\n  mounted: React.RefObject<boolean>;\n  reduced: boolean;\n  separator: React.ReactNode;\n};\n\nconst MotionListContext = React.createContext<MotionListContextValue | null>(\n  null,\n);\n\nfunction useMotionList(part: string): MotionListContextValue {\n  const context = React.useContext(MotionListContext);\n  if (!context) throw new Error(`${part} must be rendered inside MotionList.`);\n  return context;\n}\n\nexport interface MotionListProps extends useRender.ComponentProps<\"ul\"> {\n  /**\n   * `stack` (default) is for vertical lists: an item expands from height 0 and\n   * fades in, leaves by fading and collapsing, and the items below glide.\n   * `flow` is for wrapping or horizontal sets such as chips, tags and tiles: an\n   * item fades and scales slightly and its siblings glide to their new place.\n   */\n  layout?: MotionLayout;\n  /**\n   * The space between the items of a `stack`, in steps of the spacing scale (3\n   * is 12 px). There is none above the first item or below the last. It opens\n   * with an item that is added after another and closes when an item becomes\n   * the last, together with that item's own expand or collapse. A CSS `gap` on\n   * the list would leave a hole that closes only after the item has left. In\n   * `flow` set the spacing with CSS `gap` instead.\n   */\n  gap?: number;\n  /**\n   * Drawn in the space between two items, such as a divider or a connecting\n   * line, and shown and hidden with it. Needs `gap`. The content may be\n   * positioned against the item (it is the item's positioning context if the\n   * item is `relative`) or lay out inside the space.\n   */\n  separator?: React.ReactNode;\n  /**\n   * `flow` only: the element whose height follows when a row of items appears\n   * or goes, so what is below it glides instead of jumping. It is the list\n   * itself by default. A list that is `display: contents` has no box of its\n   * own: pass the element that wraps its items.\n   */\n  container?: React.RefObject<HTMLElement | null> | undefined;\n}\n\n/**\n * A list whose items animate in, out and into their new place. Children are\n * `MotionItem`s with a stable `key`. Owns the presence for the whole kit.\n */\nexport function MotionList({\n  children,\n  className,\n  container,\n  gap,\n  layout = \"stack\",\n  render,\n  separator,\n  ...props\n}: MotionListProps): React.ReactElement {\n  const listRef = React.useRef<HTMLUListElement>(null);\n  const reduced = useReducedMotionPreference();\n  const stack = layout === \"stack\";\n  const present = React.Children.toArray(children).filter(React.isValidElement);\n  const { entries, remove } = useLeavingEntries(present, stack && !reduced);\n  // Items glide to their new place when the set or the order of keys changes.\n  const layoutDependency = present.map((child) => String(child.key)).join(\"|\");\n  // The items of the first frame are at rest; items that come later animate\n  // in. A collection that renders its items one commit after the list, as\n  // React Aria's do, renders them before that frame, so they are at rest too.\n  const mounted = React.useRef(false);\n  React.useEffect(() => {\n    if (typeof requestAnimationFrame === \"undefined\") {\n      mounted.current = true;\n      return;\n    }\n    const frame = requestAnimationFrame(() => {\n      mounted.current = true;\n    });\n    return () => cancelAnimationFrame(frame);\n  }, []);\n\n  // A flow list moves its container's height when a row appears or goes. The\n  // items of a stack bring their own height, so nothing else has to follow.\n  const followed = container ?? listRef;\n  const follow = useHeightFollower(followed, !stack);\n  const settled = React.useRef(layoutDependency);\n  React.useLayoutEffect(() => {\n    if (settled.current === layoutDependency) return;\n    settled.current = layoutDependency;\n    if (stack) return;\n    follow();\n  }, [follow, layoutDependency, stack]);\n  const hasGap = stack && gap !== undefined && gap > 0;\n  const context = React.useMemo<MotionListContextValue>(\n    () => ({\n      gap: hasGap,\n      layout,\n      mounted,\n      reduced,\n      separator,\n    }),\n    [hasGap, layout, reduced, separator],\n  );\n\n  const defaultProps = {\n    children: (\n      <>\n        {stack ? (\n          entries.map((entry) => (\n            <MotionEntry\n              entryKey={entry.key}\n              key={entry.key}\n              last={entry.last}\n              leaving={entry.leaving}\n              onGone={remove}\n            >\n              {entry.element}\n            </MotionEntry>\n          ))\n        ) : (\n          // A leaving item is taken out of the flow and positioned from the\n          // list.\n          <PopLayoutPresence>{present}</PopLayoutPresence>\n        )}\n        {/* After the items, whose presence has started by then: an item that\n            enters a stack takes no height yet, so the others do not move. */}\n        <GlideOnChange\n          change={layoutDependency}\n          origin={() => followed.current}\n          parts={() => listItems(listRef.current)}\n          reduced={reduced}\n        />\n      </>\n    ),\n    className: cn(\n      slotClass(\"motion-list\", props, render),\n      !stack && \"relative\",\n      className,\n    ),\n    \"data-layout\": layout,\n    \"data-slot\": \"motion-list\",\n    style: hasGap\n      ? ({\n          \"--sp-motion-gap\": `calc(var(--spacing) * ${gap})`,\n        } as React.CSSProperties)\n      : undefined,\n  };\n  const element = useRender({\n    defaultTagName: \"ul\",\n    props: mergeProps<\"ul\">(defaultProps, props),\n    ref: listRef,\n    render,\n  });\n\n  return (\n    <MotionListContext.Provider value={context}>\n      {element}\n    </MotionListContext.Provider>\n  );\n}\n\n/** The elements of a list's items: its children in the DOM. */\nfunction listItems(list: HTMLElement | null): HTMLElement[] {\n  return list\n    ? [...list.children].filter(\n        (child): child is HTMLElement => child instanceof HTMLElement,\n      )\n    : [];\n}\n\nexport interface MotionItemProps extends NativeProps<\"li\"> {\n  /**\n   * Replaces the element, `li` by default. Pass an element (`<div />`) or a\n   * component element (`<Row />`) that forwards its ref; it gains the motion\n   * behavior. The element is the clipping box of a `stack` item, so give it no\n   * padding or border and put those on the content inside.\n   */\n  render?: React.ReactElement;\n}\n\n/**\n * One entry of a `MotionList`. Give it a stable `key`. While it leaves, it is\n * inert and hidden from assistive technology.\n */\nexport function MotionItem({\n  children,\n  className,\n  render,\n  ...props\n}: MotionItemProps): React.ReactElement {\n  const list = useMotionList(\"MotionItem\");\n  const entry = React.useContext(MotionEntryContext);\n  const ref = React.useRef<HTMLLIElement>(null);\n  const stack = list.layout === \"stack\";\n  // A stack keeps its leaving items itself (MotionEntry) and collapses them; a\n  // flow list holds them in AnimatePresence, out of the flow, until their exit\n  // has played. A stack item never holds the presence of a swap or a reveal\n  // around it, but it is inert while that presence leaves.\n  const inPresence = useIsPresent();\n  const [inFlow, safeToRemove] = usePresence(!stack);\n  const shown = stack ? !entry?.leaving : inFlow;\n  const present = shown && inPresence;\n  // The items of the first render are at rest; later ones animate in.\n  const appear = React.useRef(list.mounted.current).current;\n  const claim = entry?.claim;\n  React.useLayoutEffect(() => {\n    claim?.();\n  }, [claim]);\n  usePresenceMotion(ref, stack ? \"height\" : \"scale\", {\n    appear,\n    onGone: stack ? entry?.remove : (safeToRemove ?? undefined),\n    present: shown,\n    reduced: list.reduced,\n  });\n\n  const defaultProps = {\n    ...leavingProps(present, list.reduced),\n    // The space after the item is a block inside it, so it collapses with the\n    // item, and it is open only while another item follows.\n    children:\n      stack && list.gap ? (\n        <>\n          {children}\n          <MotionReveal data-slot=\"motion-gap\" open={!(entry?.last ?? true)}>\n            <div aria-hidden=\"true\" className=\"h-(--sp-motion-gap)\" />\n            {list.separator}\n          </MotionReveal>\n        </>\n      ) : (\n        children\n      ),\n    // The render element keeps its own props as a motion element, a data-slot\n    // among them.\n    className: cn(\n      slotClass(\"motion-item\", props, render),\n      stack && \"flow-root\",\n      className,\n    ),\n    \"data-layout\": list.layout,\n    \"data-slot\": \"motion-item\",\n  };\n\n  return useRender({\n    defaultTagName: \"li\",\n    props: mergeProps<\"li\">(defaultProps, props),\n    ref,\n    render,\n  });\n}\n\nexport interface MotionRevealProps extends NativeProps<\"div\"> {\n  /** Shows the block. It expands with a fade; hiding collapses it. */\n  open: boolean;\n  /**\n   * Replaces the element, `div` by default. It is the clipping box, so give it\n   * no padding or border and put those on the content inside.\n   */\n  render?: React.ReactElement;\n}\n\n/**\n * One block that shows or hides with `open`, for messages, field errors,\n * alerts and save bars. Nothing is rendered while it is closed, so it adds no\n * space to the layout around it.\n */\nexport function MotionReveal({\n  children,\n  open,\n  ...props\n}: MotionRevealProps): React.ReactElement {\n  return (\n    <AnimatePresence initial={false}>\n      {open ? (\n        <MotionRevealPanel key=\"panel\" {...props}>\n          {children}\n        </MotionRevealPanel>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nfunction MotionRevealPanel({\n  children,\n  className,\n  render,\n  ...props\n}: Omit<MotionRevealProps, \"open\">): React.ReactElement {\n  const [present, safeToRemove] = usePresence();\n  const reduced = useReducedMotionPreference();\n  const ref = React.useRef<HTMLDivElement>(null);\n  // A reveal that is open when it mounts (initial={false}) is at rest.\n  const appear = React.useContext(PresenceContext)?.initial !== false;\n  usePresenceMotion(ref, \"height\", {\n    appear,\n    onGone: safeToRemove ?? undefined,\n    present,\n    reduced,\n  });\n  const defaultProps = {\n    ...leavingProps(present, reduced),\n    children,\n    className: cn(\n      slotClass(\"motion-reveal\", props, render),\n      \"flow-root\",\n      className,\n    ),\n    \"data-slot\": \"motion-reveal\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    ref,\n    render,\n  });\n}\n\nexport interface MotionSwapProps extends useRender.ComponentProps<\"div\"> {\n  /**\n   * Identifies the content. When it changes, the old content fades out, the\n   * new content fades in after it and the container's height follows.\n   */\n  swapKey: React.Key;\n}\n\n/**\n * Content that replaces other content, for steps, states and panels. The old\n * content fades out quickly and the new content fades in after it, so the two\n * are never legible on top of each other. The container's height follows the\n * new content over the whole swap, so nothing jumps.\n */\nexport function MotionSwap({\n  children,\n  className,\n  render,\n  swapKey,\n  ...props\n}: MotionSwapProps): React.ReactElement {\n  const swapRef = React.useRef<HTMLDivElement>(null);\n  // Measured from the container when a swap starts.\n  const resolve = React.useCallback(\n    () => measureSwapTiming(swapRef.current),\n    [],\n  );\n\n  // The container follows the height of the new content, and keeps following\n  // the entering pane until the swap is over, so content that builds a frame\n  // later does not make the height pop at the end.\n  const follow = useHeightFollower(swapRef);\n  const previousKey = React.useRef(swapKey);\n  React.useLayoutEffect(() => {\n    if (previousKey.current === swapKey) return;\n    previousKey.current = swapKey;\n    const container = swapRef.current;\n    if (!container) return;\n    // The follow rule moves a swap's height over the whole swap, base in all.\n    follow({\n      enter: container.querySelector(\n        ':scope > [data-slot=\"motion-swap-pane\"]:not([inert])',\n      ),\n    });\n  }, [follow, swapKey]);\n\n  const defaultProps = {\n    children: (\n      <PopLayoutPresence>\n        <MotionSwapPane key={swapKey} resolve={resolve}>\n          {children}\n        </MotionSwapPane>\n      </PopLayoutPresence>\n    ),\n    // The leaving pane is taken out of the flow and positioned from here.\n    className: cn(\n      slotClass(\"motion-swap\", props, render),\n      \"relative\",\n      className,\n    ),\n    \"data-slot\": \"motion-swap\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    ref: swapRef,\n    render,\n  });\n}\n\nfunction MotionSwapPane({\n  children,\n  resolve,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  resolve: () => SwapTiming;\n}): React.ReactElement {\n  const present = useIsPresent();\n  const reduced = useReducedMotionPreference();\n  const defaultProps = {\n    ...swapPresence({ reduced, resolve }),\n    ...leavingProps(present, reduced),\n    children,\n    className: cn(slotClass(\"motion-swap-pane\", props), \"flow-root\"),\n    \"data-slot\": \"motion-swap-pane\",\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    props: mergeProps<\"div\">(defaultProps, props),\n    render: toMotionElement(undefined, \"div\"),\n  });\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion.tsx"},{"path":"packages/ui/src/components/motion/motion-presence.ts","content":"\"use client\";\n\nimport { motion, type Transition } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { getMotionTransitionFromCss } from \"../../system/motion/css-transition\";\n\n/**\n * The timing and the moves of the Motion parts, kept apart from the components\n * so they can be checked without a browser. Durations and easings are measured\n * from the `--motion-*` tokens of the element itself when an animation starts\n * (rulebook M1), so a token set on any container around it applies; nothing\n * here invents a timing. Presence and glides run on the Web Animations API from\n * the frame after a change; MotionSwap's fade and the rolling number hand their\n * values to Motion. Reduced motion (M5) makes everything instant.\n */\n\nexport type MotionLayout = \"stack\" | \"flow\";\n\n/** Used whenever motion is reduced (M5) and on the server. */\nexport const instantTransition: Transition = { duration: 0 };\n\n/**\n * The timing of an animation, measured from the element when it starts, which\n * applies a token set on any container around it. Without a document (server\n * rendering) nothing animates.\n */\nexport function measureTiming(\n  element: Element | null,\n  durationVariable: `--motion-${string}`,\n  easingVariable: `--motion-${string}`,\n): Transition {\n  if (typeof document === \"undefined\") return instantTransition;\n  return getMotionTransitionFromCss(\n    element ?? document.documentElement,\n    durationVariable,\n    easingVariable,\n  );\n}\n\n/**\n * Runs `start` in the frame after the one that renders the current commit and\n * returns its cancel. That first frame shows where a motion starts. A browser\n * can take long to present it (in Chrome the first change after an input\n * waits tens of milliseconds), and a clock started in it is already part-way\n * by the next frame; started a frame later, the motion begins where the first\n * frame left it. Base UI's panels start their transitions a frame later too.\n */\nexport function inSecondFrame(start: () => void): () => void {\n  let id = requestAnimationFrame(() => {\n    id = requestAnimationFrame(start);\n  });\n  return () => cancelAnimationFrame(id);\n}\n\n/** A unitless token such as a scale, with the value the kit ships as fallback. */\nexport function measureNumber(\n  element: Element | null,\n  variable: `--motion-${string}`,\n  fallback: number,\n): number {\n  if (typeof document === \"undefined\") return fallback;\n  const parsed = Number.parseFloat(\n    getComputedStyle(element ?? document.documentElement).getPropertyValue(\n      variable,\n    ),\n  );\n  return parsed > 0 && parsed <= 1 ? parsed : fallback;\n}\n\n/**\n * The duration and the curve of a keyframe animation, read from the motion\n * tokens of the element when it starts (rulebook M1), so a token set on any\n * container around it applies.\n */\nexport function keyframeTiming(\n  element: Element,\n  durationVariable: `--motion-${string}`,\n  easingVariable: `--motion-${string}`,\n): KeyframeAnimationOptions {\n  const styles = getComputedStyle(element);\n  const duration = styles.getPropertyValue(durationVariable).trim();\n  const easing = styles.getPropertyValue(easingVariable).trim();\n  return {\n    duration:\n      (Number.parseFloat(duration) || 0) * (duration.endsWith(\"ms\") ? 1 : 1000),\n    ...(easing ? { easing } : {}),\n  };\n}\n\n/**\n * Starts a keyframe animation that shows its first keyframe in the frame the\n * commit renders and plays from the frame after (`inSecondFrame`). The Web\n * Animations API runs it: translate, scale and opacity on the compositor,\n * height on the main thread.\n */\nexport function playFromSecondFrame(\n  element: Element,\n  keyframes: Keyframe[],\n  options: KeyframeAnimationOptions,\n): Animation {\n  const animation = element.animate(keyframes, options);\n  animation.pause();\n  inSecondFrame(() => {\n    // An animation that was cancelled or replaced meanwhile stays so.\n    if (animation.playState === \"paused\") animation.play();\n  });\n  return animation;\n}\n\n/** The element's own `overflow-y` while a height animation clips it. */\nconst clipped = new WeakMap<HTMLElement, string>();\n\nfunction clip(element: HTMLElement) {\n  if (clipped.has(element)) return;\n  clipped.set(element, element.style.overflowY);\n  element.style.overflowY = \"clip\";\n}\n\nfunction unclip(element: HTMLElement) {\n  const own = clipped.get(element);\n  if (own === undefined) return;\n  clipped.delete(element);\n  element.style.overflowY = own;\n}\n\n/** A uniform `scale` as getComputedStyle reports it: \"none\" is 1. */\nfunction scaleOf(value: string): number {\n  const scale = Number.parseFloat(value);\n  return Number.isFinite(scale) ? scale : 1;\n}\n\n/**\n * How an element comes and goes. `height`: a block that opens and closes its\n * height with a fade, a stack item or a reveal; what is below it moves with its\n * height, and it clips its content while the height moves (at rest nothing is\n * clipped, so focus rings, shadows and menus at its edge stay whole). `scale`:\n * an item of a wrapping or horizontal set, a flow list, which fades in and\n * grows from `--motion-presence-scale` and fades and shrinks out, without\n * travel (M2), while its siblings glide (`GlideOnChange`).\n */\nexport type PresenceKind = \"height\" | \"scale\";\n\n/**\n * The keyframe an element shows now: the frame a running motion is at, or,\n * with nothing running, the element at rest.\n */\nconst presenceNow: Record<PresenceKind, (element: HTMLElement) => Keyframe> = {\n  height: (element) => ({\n    height: `${element.getBoundingClientRect().height}px`,\n    opacity: Number(getComputedStyle(element).opacity),\n  }),\n  scale: (element) => {\n    const styles = getComputedStyle(element);\n    return { opacity: Number(styles.opacity), scale: scaleOf(styles.scale) };\n  },\n};\n\n/** The keyframe of an element that is gone. */\nconst presenceGone: Record<PresenceKind, (element: HTMLElement) => Keyframe> = {\n  height: () => ({ height: \"0px\", opacity: 0 }),\n  scale: (element) => ({\n    opacity: 0,\n    scale: measureNumber(element, \"--motion-presence-scale\", 0.96),\n  }),\n};\n\n/**\n * Starts the presence motion of an element over the expand duration: towards\n * rest when it is present, towards gone when it is not. A motion that still\n * runs is replaced from what it shows, so a change of course does not jump\n * back. `then` runs when the motion has finished.\n */\nexport function playPresence(\n  element: HTMLElement,\n  kind: PresenceKind,\n  present: boolean,\n  running: Animation | null,\n  then: () => void,\n): Animation {\n  const from = running\n    ? presenceNow[kind](element)\n    : present\n      ? presenceGone[kind](element)\n      : null;\n  running?.cancel();\n  const atRest = presenceNow[kind](element);\n  if (kind === \"height\") clip(element);\n  const animation = playFromSecondFrame(\n    element,\n    present ? [from!, atRest] : [from ?? atRest, presenceGone[kind](element)],\n    {\n      ...keyframeTiming(\n        element,\n        \"--motion-duration-expand\",\n        \"--motion-ease-standard\",\n      ),\n      // A leaving element keeps its last keyframe until it is removed.\n      fill: present ? \"none\" : \"forwards\",\n    },\n  );\n  animation.onfinish = () => {\n    if (present) unclip(element);\n    then();\n  };\n  return animation;\n}\n\n/**\n * Plays the presence of an element (rulebook M6, M7) over the expand duration:\n * from gone to rest when it appears or comes back, from rest to gone when it\n * leaves. JavaScript measures and the Web Animations API animates, from the\n * second frame (`playFromSecondFrame`), so a slow first frame never shows the\n * motion half done. A change of course starts from what is shown, so nothing\n * jumps back. `appear` is false for an element that is at rest when it mounts.\n * `onGone` runs when a leaving element has finished, and at once with reduced\n * motion, where nothing animates (M5).\n */\nexport function usePresenceMotion(\n  ref: React.RefObject<HTMLElement | null>,\n  kind: PresenceKind,\n  {\n    appear,\n    onGone,\n    present,\n    reduced,\n  }: {\n    appear: boolean;\n    onGone?: (() => void) | undefined;\n    present: boolean;\n    reduced: boolean;\n  },\n): void {\n  const running = React.useRef<Animation | null>(null);\n  const was = React.useRef<boolean | null>(null);\n  const gone = React.useRef(onGone);\n  gone.current = onGone;\n\n  React.useLayoutEffect(() => {\n    const element = ref.current;\n    const first = was.current === null;\n    const wasPresent = was.current;\n    was.current = present;\n    if (!element) return;\n    const current = running.current;\n    if (reduced || typeof element.animate !== \"function\") {\n      current?.cancel();\n      running.current = null;\n      unclip(element);\n      if (!present) gone.current?.();\n      return;\n    }\n    if (present && (first ? !appear : wasPresent)) return;\n    const animation = playPresence(element, kind, present, current, () => {\n      if (running.current !== animation) return;\n      running.current = null;\n      if (!present) gone.current?.();\n    });\n    running.current = animation;\n  }, [appear, kind, present, reduced, ref]);\n\n  React.useEffect(() => () => running.current?.cancel(), []);\n}\n\n/**\n * Where an element is seen against the box of `origin`. A glide starts where\n * the element was seen, also when the commit scrolled a list that scrolls\n * (scroll anchoring does when items move), so the place is not taken against\n * the scrolled content.\n */\ntype Place = { height: number; left: number; top: number; width: number };\n\nfunction placeIn(element: Element, originBox: DOMRect): Place {\n  const box = element.getBoundingClientRect();\n  return {\n    height: box.height,\n    left: box.left - originBox.left,\n    top: box.top - originBox.top,\n    width: box.width,\n  };\n}\n\n/**\n * Where the parts were before a commit, against the box the origin had then.\n * The origin can move in the commit itself, such as a centred set that loses\n * a part, so a glide adds the origin's own move to start where the part was\n * seen.\n */\ntype Before = { origin: DOMRect; places: Map<HTMLElement, Place> };\n\n/** The glide that runs on a part, to measure where it stands when it changes. */\nconst glides = new WeakMap<Element, Animation>();\n\n/** Marks the picture of a part that has left; it is no part itself. */\nexport const motionGhostAttribute = \"data-motion-ghost\";\n\nexport type GlideOnChangeProps = {\n  /** What the set is: a new value means it changed. */\n  change: unknown;\n  /** The element the parts are placed against; it has a box of its own. */\n  origin: () => HTMLElement | null;\n  /** The elements that glide. */\n  parts: () => Iterable<HTMLElement>;\n  /**\n   * The set's parts come and go without a presence of their own: a new part\n   * fades in, and a part that has left stays as a picture that fades out\n   * where it was and takes no input.\n   */\n  presence?: boolean | undefined;\n  reduced: boolean;\n};\n\n/**\n * Glides the parts of a set to their new place when the set changes (rulebook\n * M7), as a FLIP: Vue's TransitionGroup and Svelte's animate:flip move items\n * the same way. Before the commit it measures where each part is, after it\n * where each one is now; every part then shows at its old place in the frame\n * the commit renders and moves home on the compositor from the frame after\n * (`playFromSecondFrame`), with a translate added to its own. A part that is\n * still on its way glides on from where it stands. Measured in\n * getSnapshotBeforeUpdate, as Motion's layout animations measure, so it\n * renders nothing. The lifecycle methods are marked `override`, so an app\n * that compiles the installed source with `noImplicitOverride` passes.\n */\nexport class GlideOnChange extends React.Component<GlideOnChangeProps> {\n  override getSnapshotBeforeUpdate(\n    previous: Readonly<GlideOnChangeProps>,\n  ): Before | null {\n    if (previous.change === this.props.change || this.props.reduced) {\n      return null;\n    }\n    const origin = this.props.origin();\n    if (!origin || typeof origin.animate !== \"function\") return null;\n    const originBox = origin.getBoundingClientRect();\n    const places = new Map<HTMLElement, Place>();\n    for (const part of this.props.parts())\n      places.set(part, placeIn(part, originBox));\n    return { origin: originBox, places };\n  }\n\n  override componentDidUpdate(\n    _previous: Readonly<GlideOnChangeProps>,\n    _state: unknown,\n    before: Before | null,\n  ): void {\n    if (before) glideParts(this.props, before);\n  }\n\n  override render(): null {\n    return null;\n  }\n}\n\nfunction glideParts(\n  { origin: originOf, parts: partsOf, presence }: GlideOnChangeProps,\n  { origin: originBefore, places: before }: Before,\n) {\n  const origin = originOf();\n  if (!origin) return;\n  const parts = [...partsOf()];\n  // A part that still glides stood where it was measured; it goes on from\n  // there, measured now without its glide.\n  for (const part of parts) glides.get(part)?.cancel();\n  const originBox = origin.getBoundingClientRect();\n  // How far the origin itself moved in the commit, in the page.\n  const moved = {\n    left: originBefore.left - originBox.left,\n    top: originBefore.top - originBox.top,\n  };\n  const timing = keyframeTiming(\n    origin,\n    \"--motion-duration-expand\",\n    \"--motion-ease-standard\",\n  );\n  for (const part of parts) {\n    const was = before.get(part);\n    if (!was) {\n      if (presence) fadeIn(part, timing);\n      continue;\n    }\n    const now = placeIn(part, originBox);\n    const x = was.left + moved.left - now.left;\n    const y = was.top + moved.top - now.top;\n    if (Math.abs(x) < 0.5 && Math.abs(y) < 0.5) continue;\n    // A part that is leaving (Motion pops it out of the flow, against the\n    // origin) fades where it was seen, without travel (M2), also when the\n    // origin moved under it; the others glide home.\n    const leaving = part.hasAttribute(\"data-motion-pop-id\");\n    const glide = playFromSecondFrame(\n      part,\n      [\n        { translate: `${x}px ${y}px` },\n        { translate: leaving ? `${x}px ${y}px` : \"0px 0px\" },\n      ],\n      {\n        ...timing,\n        composite: \"add\",\n        ...(leaving ? { fill: \"forwards\" as const } : {}),\n      },\n    );\n    glides.set(part, glide);\n  }\n  if (!presence) return;\n  for (const [part, was] of before)\n    if (!part.isConnected)\n      leavePicture(\n        part,\n        { ...was, left: was.left + moved.left, top: was.top + moved.top },\n        origin,\n        timing,\n      );\n}\n\nfunction fadeIn(part: HTMLElement, timing: KeyframeAnimationOptions) {\n  playFromSecondFrame(\n    part,\n    [presenceGone.scale(part), presenceNow.scale(part)],\n    timing,\n  );\n}\n\n/**\n * A copy of a part that has left, at the place it had, fading out under its\n * neighbours as they glide over its place; Base UI keeps such a copy of a\n * popup's previous content for the same reason. The part itself is gone at\n * once, so focus, keyboard order and assistive technology move on as they\n * always did.\n */\nfunction leavePicture(\n  part: HTMLElement,\n  was: Place,\n  origin: HTMLElement,\n  timing: KeyframeAnimationOptions,\n) {\n  const picture = part.cloneNode(true) as HTMLElement;\n  for (const element of [picture, ...picture.querySelectorAll(\"[id]\")])\n    element.removeAttribute(\"id\");\n  Object.assign(picture.style, {\n    height: `${was.height}px`,\n    left: `${was.left}px`,\n    margin: \"0\",\n    position: \"absolute\",\n    top: `${was.top}px`,\n    width: `${was.width}px`,\n  });\n  // The holder comes first in the origin, so the picture paints under the\n  // parts that glide over its place, and it is no part's neighbour: a rule for\n  // what follows a part does not match. It covers the padding box, where an\n  // absolute child sits, from the origin's box: the picture stands where the\n  // part was seen, and scrolls with the content.\n  const holder = origin.ownerDocument.createElement(\"div\");\n  holder.setAttribute(motionGhostAttribute, \"\");\n  holder.setAttribute(\"aria-hidden\", \"true\");\n  holder.inert = true;\n  Object.assign(holder.style, {\n    height: `${origin.clientHeight}px`,\n    left: `${origin.scrollLeft - origin.clientLeft}px`,\n    pointerEvents: \"none\",\n    position: \"absolute\",\n    top: `${origin.scrollTop - origin.clientTop}px`,\n    width: `${origin.clientWidth}px`,\n  });\n  holder.append(picture);\n  origin.prepend(holder);\n  const fade = playFromSecondFrame(\n    picture,\n    [\n      { opacity: Number(getComputedStyle(picture).opacity) },\n      {\n        opacity: 0,\n        scale: measureNumber(picture, \"--motion-presence-scale\", 0.96),\n      },\n    ],\n    { ...timing, fill: \"forwards\" },\n  );\n  fade.onfinish = () => holder.remove();\n  fade.oncancel = () => holder.remove();\n}\n\ntype Timed = {\n  /** Reduced motion: every animation is instant. */\n  reduced: boolean;\n  /** The timing of an animation, measured when it starts. */\n  resolve: () => Transition;\n};\n\n/**\n * A character of a rolling number: it opens its width and fades in when it\n * appears and closes its width and fades out when it goes, so the characters\n * beside it glide instead of jumping. The cells of the first render are at\n * rest, and a cell that has left plays its collapse while it stays in place.\n */\nexport function glyphPresence({\n  appear,\n  leaving,\n  reduced,\n  resolve,\n}: Timed & { appear: boolean; leaving: boolean }) {\n  const timing = () => (reduced ? instantTransition : resolve());\n  const variants = {\n    collapsed: () => ({ opacity: 0, width: 0, transition: timing() }),\n    expanded: () => ({ opacity: 1, width: \"auto\", transition: timing() }),\n  };\n  if (reduced) {\n    return { animate: \"expanded\", initial: false, variants } as const;\n  }\n  return {\n    animate: leaving ? \"collapsed\" : \"expanded\",\n    initial: appear ? \"collapsed\" : false,\n    variants,\n  } as const;\n}\n\nexport type SwapTiming = {\n  /** The new content: starts late enough that the old content is nearly gone. */\n  enter: Transition;\n  /** The container height, over the whole swap. */\n  height: Transition;\n  /** The old content. */\n  leave: Transition;\n};\n\n/**\n * A swap takes `--motion-duration-base` in all: the old content fades out\n * within `--motion-duration-micro` and the new content fades in during the last\n * `--motion-duration-micro`, so the two overlap only where each is nearly\n * invisible (2 x micro - base, 20 ms by default). The container's height follows\n * over the whole swap. If the tokens leave no room, both fades run together.\n */\nexport function measureSwapTiming(element: Element | null): SwapTiming {\n  const leave = measureTiming(\n    element,\n    \"--motion-duration-micro\",\n    \"--motion-ease-soft-in-out\",\n  );\n  const enter = measureTiming(\n    element,\n    \"--motion-duration-micro\",\n    \"--motion-ease-standard\",\n  );\n  const height = measureTiming(\n    element,\n    \"--motion-duration-base\",\n    \"--motion-ease-standard\",\n  );\n  const delay = Math.max(\n    0,\n    ((height as { duration?: number }).duration ?? 0) -\n      ((enter as { duration?: number }).duration ?? 0),\n  );\n  return { enter: { ...enter, delay }, height, leave };\n}\n\n/**\n * The same fade through for a pane that Base UI mounts and unmounts and fades\n * with CSS transitions, such as a Tabs panel: the pane that takes over fades in\n * during the last micro duration of the base duration, the pane that leaves\n * fades out at once. The class is the kit's swap-fade rule in motion.css, which\n * names the tokens `measureSwapTiming` measures, so a swap looks the same\n * whichever way it is driven, and which is instant with reduced motion (M5).\n */\nexport const swapFadeClassName = \"sp-swap-fade\";\n\n/**\n * A Tabs panel switches at once instead (Lukas, 13 October 2026): the panel that\n * leaves is hidden the moment the new one shows, as tabs do in the WAI-ARIA\n * pattern and most interfaces. A dissolve showed both panels' text at once\n * wherever a panel has no surface of its own, and a fade through of a whole\n * panel leaves a moment with neither. The indicator and the height of the\n * panels' container carry the motion; the height follows over the expand\n * duration (the follow rule in motion.css). The class is the kit's\n * panel-switch rule in motion.css.\n */\nexport const panelSwitchClassName = \"sp-panel-switch\";\n\nconst instantSwap: SwapTiming = {\n  enter: instantTransition,\n  height: instantTransition,\n  leave: instantTransition,\n};\n\n/**\n * Fade through: the old pane fades out quickly, the new pane fades in after it,\n * so the two texts are never legible on top of each other.\n */\nexport function swapPresence({\n  reduced,\n  resolve,\n}: {\n  reduced: boolean;\n  resolve: () => SwapTiming;\n}) {\n  const timing = () => (reduced ? instantSwap : resolve());\n  const variants = {\n    hidden: () => ({ opacity: 0, transition: timing().leave }),\n    shown: () => ({ opacity: 1, transition: timing().enter }),\n  };\n  if (reduced) {\n    return { animate: \"shown\", initial: false, variants } as const;\n  }\n  return {\n    animate: \"shown\",\n    exit: \"hidden\",\n    initial: \"hidden\",\n    variants,\n  } as const;\n}\n\ntype Keyed = { key: React.Key | null };\n\n/**\n * An item that has left the list and still plays its exit. `after` is the key\n * of the item that preceded it when it left (`null` for the first), `last`\n * whether it was the last one, and `element` what it rendered.\n */\nexport type Leaving<T = unknown> = {\n  after: React.Key | null;\n  element: T;\n  key: React.Key;\n  last: boolean;\n};\n\n/**\n * The keys in the order they render: the present items, with each leaving item\n * right after the item it followed. Where the present items moved or others\n * were inserted before it, a leaving item stays with its neighbour instead of\n * keeping its old index, so it never glides across the item beside it.\n */\nexport function mergeLeaving(\n  present: readonly React.Key[],\n  leaving: ReadonlyArray<Pick<Leaving, \"after\" | \"key\">>,\n): React.Key[] {\n  const order = [...present];\n  const pending = leaving.filter((entry) => !order.includes(entry.key));\n  while (pending.length > 0) {\n    // An item waits until the item it followed is placed; chains are acyclic.\n    const ready = pending.findIndex(\n      (entry) => entry.after === null || order.includes(entry.after),\n    );\n    const [entry] = pending.splice(Math.max(0, ready), 1);\n    order.splice(\n      entry!.after === null ? 0 : order.indexOf(entry!.after) + 1,\n      0,\n      entry!.key,\n    );\n  }\n  return order;\n}\n\n/**\n * The leaving items after an update. `previous` is what the list rendered\n * before it (the present items of the last commit) and `leaving` what was\n * leaving then. An item that is no longer present starts to leave, remembering\n * the item it followed; an item that came back stops. The same array comes back\n * when nothing changed.\n */\nexport function nextLeaving<T extends Keyed>(\n  leaving: ReadonlyArray<Leaving<T>>,\n  previous: readonly T[],\n  present: readonly T[],\n): ReadonlyArray<Leaving<T>> {\n  const presentKeys = new Set(present.map((item) => item.key as React.Key));\n  const next = leaving.filter((entry) => !presentKeys.has(entry.key));\n  const known = new Set(next.map((entry) => entry.key));\n  const order = mergeLeaving(\n    previous.map((item) => item.key as React.Key),\n    leaving,\n  );\n  const lastKey = previous.at(-1)?.key;\n  const byKey = new Map(previous.map((item) => [item.key, item]));\n  order.forEach((key, index) => {\n    if (presentKeys.has(key) || known.has(key)) return;\n    const element = byKey.get(key as string);\n    if (!element) return;\n    known.add(key);\n    next.push({\n      after: index > 0 ? order[index - 1]! : null,\n      element,\n      key,\n      last: key === lastKey,\n    });\n  });\n  return next.length === leaving.length &&\n    next.every((entry, index) => entry === leaving[index])\n    ? leaving\n    : next;\n}\n\n/** One entry of a stack list as it renders: a present item or a leaving one. */\nexport type StackEntry<T> = {\n  element: T;\n  key: React.Key;\n  /** Whether no other present item follows, so no space is needed after it. */\n  last: boolean;\n  leaving: boolean;\n};\n\n/**\n * Every entry of a stack list in render order, with the leaving items in their\n * place. A leaving item keeps the `last` it had when it left, so the space after\n * it does not open or close during its exit.\n */\nexport function stackEntries<T extends Keyed>(\n  present: readonly T[],\n  leaving: ReadonlyArray<Leaving<T>>,\n): StackEntry<T>[] {\n  const lastKey = present.at(-1)?.key;\n  const byKey = new Map(present.map((item) => [item.key, item]));\n  const held = new Map(leaving.map((entry) => [entry.key, entry]));\n  return mergeLeaving(\n    present.map((item) => item.key as React.Key),\n    leaving,\n  ).map((key) => {\n    const element = byKey.get(key as string);\n    if (element) return { element, key, last: key === lastKey, leaving: false };\n    const entry = held.get(key)!;\n    return { element: entry.element, key, last: entry.last, leaving: true };\n  });\n}\n\n/**\n * The leaving items once one has finished its exit. Items that followed it\n * follow what it followed, so the order of the others does not change.\n */\nexport function dropLeaving<T>(\n  leaving: ReadonlyArray<Leaving<T>>,\n  key: React.Key,\n): ReadonlyArray<Leaving<T>> {\n  const gone = leaving.find((entry) => entry.key === key);\n  if (!gone) return leaving;\n  return leaving\n    .filter((entry) => entry !== gone)\n    .map((entry) =>\n      entry.after === key ? { ...entry, after: gone.after } : entry,\n    );\n}\n\n/**\n * The entries of a keyed list, leaving ones included. An item that is no longer\n * given stays in the list until its exit has played, in the place after the\n * item it followed; `remove` drops it once it has gone. `track` is off for\n * lists whose items leave at once, and for reduced motion, where nothing stays.\n */\nexport function useLeavingEntries<T extends Keyed>(\n  present: readonly T[],\n  track: boolean,\n) {\n  const [leaving, setLeaving] = React.useState<ReadonlyArray<Leaving<T>>>([]);\n  // What the list rendered in the last commit; a leaving item shows it again.\n  const committed = React.useRef(present);\n  React.useLayoutEffect(() => {\n    committed.current = present;\n  });\n\n  const next = track\n    ? nextLeaving(leaving, committed.current, present)\n    : leaving.length > 0\n      ? []\n      : leaving;\n  if (next !== leaving) setLeaving(next);\n\n  const remove = React.useCallback(\n    (key: React.Key) => setLeaving((entries) => dropLeaving(entries, key)),\n    [],\n  );\n  return { entries: stackEntries(present, next), remove };\n}\n\n/**\n * Clips the element while its height (or its width, for the characters of a\n * rolling number) moves and releases the clip at rest. Motion keeps `auto` in\n * the element's style at rest and a pixel value while it moves, so the style\n * itself says which. `overflow` is not a Motion target: Motion writes back every\n * value it knows on each frame and would undo the release. A style observer,\n * unlike a completion callback, cannot miss the end of an interrupted\n * animation. The returned `ref` belongs on the animated element.\n */\nexport function useClipWhileAnimating<T extends HTMLElement = HTMLElement>(\n  axis: \"height\" | \"width\" = \"height\",\n) {\n  const ref = React.useRef<T | null>(null);\n  React.useEffect(() => {\n    const element = ref.current;\n    if (!element || typeof MutationObserver === \"undefined\") return;\n    const overflow = axis === \"height\" ? \"overflowY\" : \"overflowX\";\n    const follow = () => {\n      const size = element.style[axis];\n      const next = size === \"\" || size === \"auto\" ? \"\" : \"clip\";\n      if (element.style[overflow] !== next) element.style[overflow] = next;\n    };\n    follow();\n    const observer = new MutationObserver(follow);\n    observer.observe(element, { attributeFilter: [\"style\"], attributes: true });\n    return () => observer.disconnect();\n  }, [axis]);\n  return { ref };\n}\n\n/**\n * A leaving element takes no input and is hidden from assistive technology.\n * While motion is reduced it is hidden at once, in the frame it leaves in,\n * although the element itself is removed a moment later.\n */\nexport function leavingProps(present: boolean, reduced = false) {\n  if (present) return {};\n  return {\n    \"aria-hidden\": true,\n    inert: true,\n    ...(reduced ? { style: { display: \"none\" } } : {}),\n  };\n}\n\ntype MotionTag = \"div\" | \"li\" | \"ul\";\n\nconst wrapped = new WeakMap<object, React.ElementType>();\n\n/**\n * The `render` element, upgraded to a Motion element so that it can animate:\n * a DOM element (`<div />`) becomes its `motion.*` counterpart, a component\n * element (`<Card />`) is wrapped with `motion.create`. Without `render` the\n * default tag is used. Props, key and ref of the element are kept.\n */\nexport function toMotionElement(\n  render: React.ReactElement | undefined,\n  defaultTag: MotionTag,\n): React.ReactElement {\n  if (!render)\n    return React.createElement(motion[defaultTag] as React.ElementType);\n\n  const type = render.type;\n  let component: React.ElementType;\n  if (typeof type === \"string\") {\n    component = (motion as unknown as Record<string, React.ElementType>)[type]!;\n  } else {\n    let existing = wrapped.get(type as object);\n    if (!existing) {\n      existing = motion.create(type as React.ElementType) as React.ElementType;\n      wrapped.set(type as object, existing);\n    }\n    component = existing;\n  }\n  return React.createElement(\n    component,\n    render.props as Record<string, unknown>,\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-presence.ts"},{"path":"packages/ui/src/components/motion/motion-follow.ts","content":"\"use client\";\n\nimport * as React from \"react\";\n\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport { inSecondFrame } from \"./motion-presence\";\n\n/**\n * How a container follows the height of what it holds when content comes and\n * goes: a swapped pane, a row of wrapping items, a panel that leaves while it\n * stays mounted. It is the one place that moves a container's height, so every\n * part and every component built on them moves it the same way.\n *\n * JavaScript measures and a CSS transition animates, as Base UI's Collapsible\n * and Accordion panels do with the height they measure: the container is held\n * at the height it showed, then given the height of its new content, and the\n * kit's follow rule in motion.css animates between the two. A transition starts\n * with the frame that renders it, however long the commit before that frame\n * took, so the first frame never shows the movement half done; a new height\n * that arrives mid-way retargets it from the height shown.\n */\n\nexport type FollowOptions = {\n  /**\n   * The element that takes over. It is observed until the follow is over, so\n   * content that builds a frame later, such as a collection or an editor, is\n   * followed instead of popping into place at the end.\n   */\n  enter?: Element | null;\n};\n\nexport type HeightFollow = { stop: () => void };\n\n/** The class of the follow rule (motion.css), while a container follows. */\nexport const heightFollowClassName = \"sp-height-follow\";\n\n/** A follow that runs, with the height it heads for. */\ntype Running = HeightFollow & { target: () => number };\n\n/** The follow that runs in a container, to end it before another starts. */\nconst follows = new Map<HTMLElement, Running>();\n\n/**\n * How much taller the content of `element` will be than it shows now: the\n * containers that follow inside it head for heights they do not show yet. A\n * container that follows around them heads for the height its content will\n * have, not for the frame they are at (a group of tags that builds in a\n * swapped pane).\n */\nfunction innerGrowth(element: Element): number {\n  let growth = 0;\n  for (const [inner, follow] of follows)\n    if (inner !== element && element.contains(inner))\n      growth += follow.target() - inner.getBoundingClientRect().height;\n  return growth;\n}\n\n/** The longest duration of a computed `transition-duration` list, in ms. */\nfunction longestDuration(value: string): number {\n  return Math.max(\n    0,\n    ...value\n      .split(\",\")\n      .map(\n        (part) =>\n          Number.parseFloat(part) * (part.trim().endsWith(\"ms\") ? 1 : 1000),\n      )\n      .filter(Number.isFinite),\n  );\n}\n\nconst nothing: HeightFollow = { stop: () => undefined };\n\n/**\n * Moves the container from `from`, the height it showed before its content\n * changed, to the height of the new content, measured now in the commit that\n * changed it. The container is held at `from` for the frame that commit\n * renders, and the frame after hands it the new height, which the follow rule\n * animates (`inSecondFrame`). With an entering element the follow keeps\n * watching it: when it\n * grows or shrinks, the container heads for the new height from the one it\n * shows. That is read from the observed size alone, without releasing the\n * container, so following costs no extra layout. Everything the container is\n * given (height, clip, the class) is handed back when the height has arrived,\n * so later changes flow naturally. A container that scrolls keeps its own\n * height, and so does one that had no box before (`from` is `null`). A pane\n * that leaves is positioned by `holdPanePlace`, which lasts as long as the\n * pane and not as long as the height.\n */\nexport function followHeight(\n  container: HTMLElement,\n  from: number | null,\n  { enter }: FollowOptions = {},\n): HeightFollow {\n  follows.get(container)?.stop();\n  if (from === null || typeof requestAnimationFrame === \"undefined\") {\n    return nothing;\n  }\n  if (/auto|scroll/.test(getComputedStyle(container).overflowY)) return nothing;\n\n  // The commit has laid out the new content. Besides its height, the follow\n  // keeps what the container holds around the entering element (a tab list,\n  // padding), so that the container's height can be told from the entering\n  // element's size later on.\n  let target =\n    container.getBoundingClientRect().height + innerGrowth(container);\n  const around = enter\n    ? target - enter.getBoundingClientRect().height - innerGrowth(enter)\n    : 0;\n\n  const kept = {\n    height: container.style.height,\n    overflowY: container.style.overflowY,\n  };\n  let pinned = false;\n  let pinnedAt = from;\n  let started = false;\n  let done = false;\n  let cancelHead: (() => void) | undefined;\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  // An entering element is watched; see `measured` below.\n  const observer =\n    enter && typeof ResizeObserver !== \"undefined\"\n      ? new ResizeObserver(measured)\n      : undefined;\n\n  // The container shows the height it had; what leaves is clipped with it.\n  const pin = (height: number) => {\n    pinned = true;\n    pinnedAt = height;\n    container.classList.add(heightFollowClassName);\n    container.style.overflowY = \"clip\";\n    container.style.height = `${height}px`;\n  };\n  const release = () => {\n    pinned = false;\n    container.classList.remove(heightFollowClassName);\n    container.style.height = kept.height;\n    container.style.overflowY = kept.overflowY;\n  };\n  const finish = () => {\n    if (done) return;\n    done = true;\n    cancelHead?.();\n    clearTimeout(timer);\n    observer?.disconnect();\n    container.removeEventListener(\"transitionend\", arrived);\n    release();\n    if (follows.get(container) === follow) follows.delete(container);\n  };\n  // The height has arrived when its transition ends.\n  function arrived(event: Event) {\n    if (\n      event.target === container &&\n      (event as TransitionEvent).propertyName === \"height\"\n    )\n      finish();\n  }\n  const follow: Running = { stop: finish, target: () => target };\n  follows.set(container, follow);\n\n  pin(from);\n  // How long a move takes: the follow rule's duration, or what the app's own\n  // transition on the container says. Nothing is held longer than that after\n  // the last change, also when no transition runs to say it has ended.\n  const duration = longestDuration(\n    getComputedStyle(container).transitionDuration,\n  );\n  const settleBy = () => {\n    clearTimeout(timer);\n    timer = setTimeout(finish, duration + 100);\n  };\n  container.addEventListener(\"transitionend\", arrived);\n  settleBy();\n\n  // The new height, which the rule animates from the one held. When nothing\n  // moves, an entering element is still watched for as long as a move takes;\n  // the container meanwhile has its own height again.\n  const head = () => {\n    cancelHead = undefined;\n    if (done) return;\n    if (!enter && Math.abs(target - from) < 1) {\n      finish();\n      return;\n    }\n    if (Math.abs(target - pinnedAt) < 1) release();\n    else container.style.height = `${target}px`;\n    settleBy();\n  };\n\n  if (!observer || !enter) {\n    cancelHead = inSecondFrame(head);\n    return follow;\n  }\n  observer.observe(enter);\n  return follow;\n\n  // With an entering element the height is held until that element has been\n  // laid out once, at the end of the frame the commit renders: content that\n  // builds a moment later (a collection, an editor) is then in place, and the\n  // new height follows in the frame after. The observer only reads: after\n  // layout, the sizes are known without forcing another layout.\n  function measured() {\n    if (done || !enter) return;\n    // Held, the container shows its pin or the transition's frame; handed back,\n    // it showed the height it settled at until this layout grew or shrank it.\n    const shown = pinned ? container.getBoundingClientRect().height : target;\n    const natural =\n      enter.getBoundingClientRect().height + innerGrowth(enter) + around;\n    // An entering element that fills all the room the container gives it may\n    // be stretched to it (a Tabs panel grows into its root): its size says\n    // nothing about its content until it no longer fits.\n    const fills = Math.abs(natural - shown) < 1;\n    if (!started) {\n      started = true;\n      if (!fills) target = natural;\n      cancelHead = inNextFrame(head);\n      return;\n    }\n    if (fills || Math.abs(natural - target) < 1) return;\n    target = natural;\n    if (cancelHead) return;\n    if (pinned) {\n      container.style.height = `${target}px`;\n      settleBy();\n    } else {\n      // The container rests at its own height. Held at what it shows, it\n      // heads for the new height in the next frame, so the move has a height\n      // to start from.\n      pin(shown);\n      cancelHead = inNextFrame(head);\n    }\n  }\n}\n\nfunction inNextFrame(start: () => void): () => void {\n  const id = requestAnimationFrame(start);\n  return () => cancelAnimationFrame(id);\n}\n\n/**\n * Lets a container follow its height when its content changes. Returns\n * `follow`, to call in a layout effect in the commit that changes the content:\n * the container then moves from the height it showed before the commit to the\n * height of the new content. Does nothing when motion is reduced (M5), when the\n * container had no box (hidden) or when `enabled` is false.\n */\nexport function useHeightFollower(\n  ref: React.RefObject<HTMLElement | null>,\n  enabled = true,\n): (options?: FollowOptions) => void {\n  const reduced = useReducedMotionPreference();\n  const instant = React.useRef(reduced);\n  instant.current = reduced;\n  const height = React.useRef<number | null>(null);\n  const session = React.useRef<HeightFollow | null>(null);\n\n  // The height at the last frame, including frames of a running follow. A\n  // passive effect, because the container may be an ancestor, whose ref is set\n  // after the layout effects of the component that follows it. The observer\n  // reads in the rendering steps of a frame, from the first frame on, so the\n  // height is one the page has shown: a commit that never reached the screen,\n  // such as the empty first pass of a React Aria collection, is no height to\n  // follow from.\n  React.useEffect(() => {\n    const element = ref.current;\n    if (!enabled || !element || typeof ResizeObserver === \"undefined\") return;\n    const measure = () => {\n      height.current =\n        element.getClientRects().length > 0\n          ? element.getBoundingClientRect().height\n          : null;\n    };\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [enabled, ref]);\n\n  React.useEffect(() => () => session.current?.stop(), []);\n\n  return React.useCallback(\n    (options?: FollowOptions) => {\n      const container = ref.current;\n      if (!enabled || !container || instant.current) return;\n      // A follow that still runs shows its own height; at rest it is the one\n      // seen at the last frame before this commit.\n      const from = follows.has(container)\n        ? container.getBoundingClientRect().height\n        : height.current;\n      session.current = followHeight(container, from, options);\n    },\n    [enabled, ref],\n  );\n}\n\n/** Where a pane sits in its container. */\nexport type PanePlace = {\n  height: number;\n  left: number;\n  top: number;\n  width: number;\n};\n\n/** The style that keeps a leaving pane where it is, out of the flow. */\nexport function panePin(place: PanePlace): React.CSSProperties {\n  return {\n    height: place.height,\n    left: place.left,\n    margin: 0,\n    position: \"absolute\",\n    top: place.top,\n    width: place.width,\n  };\n}\n\n/** The containers panes are pinned in: how many, and the position they had. */\nconst placeHolders = new WeakMap<\n  HTMLElement,\n  { panes: number; position: string }\n>();\n\n/**\n * Makes the container the box a leaving pane is pinned against (`panePin`): a\n * container that is not positioned becomes `relative`. Returns the release, to\n * call when the pane is gone; the container gets its own position back when the\n * last pane pinned in it is gone. The hold lasts as long as the pane, which can\n * outlast its container's height follow.\n */\nexport function holdPanePlace(container: HTMLElement): () => void {\n  const held = placeHolders.get(container);\n  if (held) {\n    held.panes += 1;\n  } else {\n    placeHolders.set(container, {\n      panes: 1,\n      position: container.style.position,\n    });\n    if (getComputedStyle(container).position === \"static\") {\n      container.style.position = \"relative\";\n    }\n  }\n  let released = false;\n  return () => {\n    const entry = placeHolders.get(container);\n    if (released || !entry) return;\n    released = true;\n    entry.panes -= 1;\n    if (entry.panes > 0) return;\n    placeHolders.delete(container);\n    container.style.position = entry.position;\n  };\n}\n\n/** The pane's place against the padding box of its container. */\nfunction placeOf(pane: HTMLElement, container: HTMLElement): PanePlace {\n  const box = pane.getBoundingClientRect();\n  const containerBox = container.getBoundingClientRect();\n  return {\n    height: box.height,\n    left:\n      box.left -\n      containerBox.left -\n      container.clientLeft +\n      container.scrollLeft,\n    top: box.top - containerBox.top - container.clientTop + container.scrollTop,\n    width: box.width,\n  };\n}\n\n/**\n * A pane that leaves while it stays mounted, as a Base UI panel does until its\n * exit transition has run, instead of being replaced by a `MotionSwap` (which\n * would remount a pane that is kept mounted, together with the state in it).\n * `open` says the pane is shown, `leaving` that it is on its way out. The\n * returned style, for the pane while it leaves, keeps it where it was, out of\n * the flow and against its container until it is gone (`holdPanePlace`), so the\n * pane that takes over has the place and the pane's container follows the new\n * height over the expand duration (the follow rule in motion.css); the panel\n * switch rule (panelSwitchClassName) hides it at once. With reduced motion the\n * height changes at once (M5).\n * `enter` finds the pane that takes over in the container, so late content in it\n * is followed; give it a stable function.\n */\nexport function useLeavingPane(\n  ref: React.RefObject<HTMLElement | null>,\n  {\n    enter,\n    leaving,\n    open,\n  }: {\n    enter?: (container: HTMLElement) => Element | null;\n    leaving: boolean;\n    open: boolean;\n  },\n): React.CSSProperties | undefined {\n  const reduced = useReducedMotionPreference();\n  const place = React.useRef<PanePlace | null>(null);\n  // The container is the element the pane sits in.\n  const container = React.useMemo(\n    () => ({\n      get current() {\n        return ref.current?.parentElement ?? null;\n      },\n    }),\n    [ref],\n  );\n  const follow = useHeightFollower(container);\n  const latest = React.useRef({ enter, follow });\n  latest.current = { enter, follow };\n\n  // The pane leaves: it is pinned against its container until it is gone, and\n  // the container follows from the height it had to the new one.\n  React.useLayoutEffect(() => {\n    const element = container.current;\n    if (!leaving || reduced || !element || !place.current) return;\n    const release = holdPanePlace(element);\n    latest.current.follow({ enter: latest.current.enter?.(element) ?? null });\n    return release;\n  }, [container, leaving, reduced]);\n\n  // At rest: where the pane sits, which is where it is pinned when it leaves.\n  React.useLayoutEffect(() => {\n    const pane = ref.current;\n    const element = pane?.parentElement;\n    if (!open || !pane || !element || typeof ResizeObserver === \"undefined\") {\n      return;\n    }\n    place.current = placeOf(pane, element);\n    const observer = new ResizeObserver(() => {\n      place.current = placeOf(pane, element);\n    });\n    observer.observe(pane);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [open, ref]);\n\n  if (!leaving) return undefined;\n  if (reduced) return { display: \"none\" };\n  return place.current ? panePin(place.current) : undefined;\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-follow.ts"},{"path":"packages/ui/src/components/motion/motion-number.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useTransform,\n  type AnimationPlaybackControls,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport {\n  defaultNumberLocales,\n  digitGlyphs,\n  localeKey,\n  mod,\n  numberCells,\n  numberTokens,\n  numberTrend,\n  stringTokens,\n  tokensValue,\n  wheelRange,\n  wheelTarget,\n  type NumberCell,\n} from \"./motion-number-cells\";\nimport {\n  glyphPresence,\n  measureTiming,\n  useClipWhileAnimating,\n  useLeavingEntries,\n} from \"./motion-presence\";\n\nexport interface MotionNumberProps extends Omit<\n  useRender.ComponentProps<\"span\">,\n  \"children\"\n> {\n  /**\n   * The value to show. A number is formatted with `locales` and `format`; a\n   * string is shown as it is, and its digits roll like the digits of a number.\n   */\n  value: number | string;\n  /**\n   * The locale a number is formatted in, as `Intl.NumberFormat` takes it.\n   * `en-US` by default, so that the server and the client always agree.\n   */\n  locales?: Intl.LocalesArgument;\n  /** `Intl.NumberFormat` options for a number: currency, percent, compact notation, signs, units. */\n  format?: Intl.NumberFormatOptions;\n}\n\ntype NumberModel = {\n  cells: NumberCell[];\n  glyphs: readonly string[];\n  /** What the value is, to tell which way it moved. */\n  scalar: number;\n  /** The text as a whole: what assistive technology reads. */\n  text: string;\n};\n\nfunction useNumberModel(\n  value: number | string,\n  locales: Intl.LocalesArgument,\n  format: Intl.NumberFormatOptions | undefined,\n): NumberModel {\n  // The content of the locales and the format is their identity: a new object\n  // with the same content is the same number, so the memo keys on the content.\n  const localesId = localeKey(locales);\n  const formatId = format ? JSON.stringify(format) : \"\";\n  return React.useMemo(() => {\n    const tokens =\n      typeof value === \"number\"\n        ? numberTokens(value, locales, format)\n        : stringTokens(value, locales);\n    return {\n      cells: numberCells(tokens),\n      glyphs: digitGlyphs(locales),\n      scalar: typeof value === \"number\" ? value : tokensValue(tokens),\n      text: tokens.map((token) => token.text).join(\"\"),\n    };\n  }, [value, localesId, formatId]);\n}\n\n// A digit rolls through a window one line tall. Its glyphs fade at the edges\n// while it rolls, and only then: at rest the digit is plain text.\nconst wheelWindowClassName =\n  \"absolute inset-0 overflow-y-clip [mask-image:linear-gradient(to_bottom,transparent,#000_18%,#000_82%,transparent)]\";\n\ntype Roll = {\n  base: number;\n  count: number;\n  from: number;\n  to: number;\n  /**\n   * The width of the place when the roll starts and when it ends: the digits of\n   * a font without tabular figures differ in width.\n   */\n  widthFrom: number;\n  widthTo: number;\n};\n\n/**\n * One digit place: the digit as plain text at rest, and while it rolls an\n * endless wheel of the ten digits. The wheel's position is a number that never\n * wraps (9 is followed by 10, which reads 0), so it rolls on in the direction of\n * the trend, and a change that arrives while it still rolls goes on from where\n * the wheel is instead of from the digit it was heading for. The width of the\n * place follows the roll from the old digit's width to the new one's, so a font\n * whose digits differ in width does not make the number jump.\n */\nfunction Wheel({\n  digit,\n  glyphs,\n  reduced,\n  trend,\n}: {\n  digit: number;\n  glyphs: readonly string[];\n  reduced: boolean;\n  trend: 1 | -1;\n}): React.ReactElement {\n  const position = useMotionValue(digit);\n  const [roll, setRoll] = React.useState<Roll | null>(null);\n  const sizer = React.useRef<HTMLSpanElement>(null);\n  const windowRef = React.useRef<HTMLSpanElement>(null);\n  const settled = React.useRef(digit);\n  const restWidth = React.useRef(0);\n  const running = React.useRef<AnimationPlaybackControls | null>(null);\n\n  // The rows move up as the position rises: row `base` is at the top at 0%.\n  const base = roll?.base ?? 0;\n  const count = roll?.count ?? 1;\n  const y = useTransform(\n    position,\n    (value) => `${(-(value - base) / count) * 100}%`,\n  );\n  const width = useTransform(position, (value) =>\n    roll\n      ? roll.widthFrom +\n        ((roll.widthTo - roll.widthFrom) * (value - roll.from)) /\n          (roll.to - roll.from)\n      : 0,\n  );\n\n  // A new digit: roll to it from wherever the wheel is, or show it at once.\n  React.useLayoutEffect(() => {\n    if (settled.current === digit) return;\n    settled.current = digit;\n    running.current?.stop();\n    // At rest the wheel stands at any whole place that reads as its digit; the\n    // places stay small. While it still rolls it goes on from where it is.\n    if (!roll) position.jump(mod(position.get(), 10));\n    const from = position.get();\n    const to = wheelTarget(from, digit, trend);\n    if (reduced || to === from) {\n      position.jump(digit);\n      setRoll(null);\n    } else {\n      setRoll({\n        ...wheelRange(from, to),\n        from,\n        to,\n        widthFrom: roll ? width.get() : restWidth.current,\n        widthTo: sizer.current?.getBoundingClientRect().width ?? 0,\n      });\n    }\n  }, [digit, position, reduced, trend]);\n\n  // At rest: how wide the digit is, which is where the next roll starts. It is\n  // measured when the place mounts, when its digit or glyphs change and when a\n  // roll ends, not after every render of the number around it.\n  React.useLayoutEffect(() => {\n    if (!roll)\n      restWidth.current = sizer.current?.getBoundingClientRect().width ?? 0;\n  }, [digit, glyphs, roll]);\n\n  // The roll: the position follows its target over the value duration.\n  React.useLayoutEffect(() => {\n    if (!roll) return;\n    const target = roll.to;\n    running.current = animate(position, target, {\n      ...measureTiming(\n        windowRef.current,\n        \"--motion-duration-value\",\n        \"--motion-ease-standard\",\n      ),\n      // The wheel stays where it stopped until the plain digit takes over: the\n      // last row shown is the digit.\n      onComplete: () => {\n        running.current = null;\n        setRoll(null);\n      },\n    });\n    return () => running.current?.stop();\n  }, [position, roll]);\n\n  return (\n    <motion.span\n      className=\"relative inline-block\"\n      {...(roll ? { style: { width } } : {})}\n    >\n      {/* The digit gives the place its baseline while it rolls. */}\n      <span className={roll ? \"invisible\" : undefined} ref={sizer}>\n        {glyphs[digit]}\n      </span>\n      {roll ? (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\"sp-motion-number-wheel\", wheelWindowClassName)}\n          data-slot=\"motion-number-wheel\"\n          ref={windowRef}\n        >\n          <motion.span className=\"block text-center\" style={{ y }}>\n            {Array.from({ length: roll.count }, (_, row) => (\n              <span className=\"block\" key={roll.base + row}>\n                {glyphs[mod(roll.base + row, 10)]}\n              </span>\n            ))}\n          </motion.span>\n        </span>\n      ) : null}\n    </motion.span>\n  );\n}\n\n/**\n * One character of the number. A place that appears opens its width and fades\n * in, a place that goes closes and fades out, so the characters beside it glide.\n * A digit that changes rolls; a character that changes is a new place.\n */\nfunction Glyph({\n  appear,\n  cell,\n  glyphs,\n  leaving,\n  onGone,\n  reduced,\n  trend,\n}: {\n  appear: boolean;\n  cell: NumberCell;\n  glyphs: readonly string[];\n  leaving: boolean;\n  onGone: (key: React.Key) => void;\n  reduced: boolean;\n  trend: 1 | -1;\n}): React.ReactElement {\n  const clip = useClipWhileAnimating<HTMLSpanElement>(\"width\");\n  // The places of the first render are at rest; later ones animate in.\n  const entering = React.useRef(appear).current;\n  const presence = glyphPresence({\n    appear: entering,\n    leaving,\n    reduced,\n    resolve: () =>\n      measureTiming(\n        clip.ref.current,\n        \"--motion-duration-value\",\n        \"--motion-ease-standard\",\n      ),\n  });\n\n  return (\n    <motion.span\n      {...presence}\n      className={cn(\n        cell.kind === \"digit\"\n          ? \"sp-motion-number-digit\"\n          : \"sp-motion-number-char\",\n        \"relative inline-block whitespace-pre\",\n      )}\n      data-slot={\n        cell.kind === \"digit\" ? \"motion-number-digit\" : \"motion-number-char\"\n      }\n      onAnimationComplete={(definition) => {\n        if (leaving && definition === \"collapsed\") onGone(cell.key);\n      }}\n      ref={clip.ref}\n    >\n      {cell.kind === \"digit\" ? (\n        <Wheel\n          digit={cell.value}\n          glyphs={glyphs}\n          reduced={reduced}\n          trend={trend}\n        />\n      ) : (\n        cell.text\n      )}\n    </motion.span>\n  );\n}\n\n/**\n * A number that rolls to its new value, for figures that change in place:\n * totals, rates, counts. Each digit slides like an odometer wheel to the new\n * digit, upward when the number rises and downward when it falls; characters\n * that come and go (a sign, a separator, a new leading digit) open and close\n * their width; tabular figures keep everything from jittering. The first\n * render and the server show the final text, and reduced motion is instant.\n * Assistive technology reads the final value once, with no live announcement.\n */\nexport function MotionNumber({\n  className,\n  format,\n  locales = defaultNumberLocales,\n  render,\n  value,\n  ...props\n}: MotionNumberProps): React.ReactElement {\n  const reduced = useReducedMotionPreference();\n  const model = useNumberModel(value, locales, format);\n  // Places that mount after the first render animate in.\n  const mounted = React.useRef(false);\n  React.useEffect(() => {\n    mounted.current = true;\n  }, []);\n\n  // Which way the number moved, from the value of the last commit.\n  const before = React.useRef(model.scalar);\n  const trend = numberTrend(before.current, model.scalar);\n  React.useLayoutEffect(() => {\n    before.current = model.scalar;\n  });\n\n  const { entries, remove } = useLeavingEntries(model.cells, !reduced);\n\n  const defaultProps = {\n    children: (\n      <>\n        <span className=\"sr-only\">{model.text}</span>\n        <span\n          aria-hidden=\"true\"\n          className=\"sp-motion-number-glyphs\"\n          data-slot=\"motion-number-glyphs\"\n        >\n          {entries.map((entry) => (\n            <Glyph\n              appear={mounted.current}\n              cell={entry.element}\n              glyphs={model.glyphs}\n              key={entry.key}\n              leaving={entry.leaving}\n              onGone={remove}\n              reduced={reduced}\n              trend={trend}\n            />\n          ))}\n        </span>\n      </>\n    ),\n    className: cn(\n      slotClass(\"motion-number\", props, render),\n      \"relative inline-block whitespace-nowrap tabular-nums\",\n      className,\n    ),\n    \"data-slot\": \"motion-number\",\n    // The places of a number are boxes of their own, which a right-to-left\n    // parent would put in reverse order. A number reads from left to right in\n    // every writing direction, and the number as a whole sits where the layout\n    // puts it. (`dir` isolates it from the text around it, too.)\n    dir: \"ltr\",\n  };\n\n  return useRender({\n    defaultTagName: \"span\",\n    props: mergeProps<\"span\">(defaultProps, props),\n    render,\n  });\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-number.tsx"},{"path":"packages/ui/src/components/motion/motion-number-cells.ts","content":"/**\n * What a rolling number is made of, kept apart from the component so that it\n * can be checked without a DOM. A number is cut into characters, and every\n * character gets a key that names the place it holds: the units digit, the\n * group separator after the thousands, the percent sign. A character that keeps\n * its place while the number changes is the same cell, so a digit rolls and a\n * sign that stays does nothing; a place that appears or goes opens or closes its\n * width. The server and the first render show the final text.\n */\n\n/** The locale of a number that names none: fixed, so the server and the client agree. */\nexport const defaultNumberLocales = \"en-US\";\n\nexport type NumberToken =\n  | { kind: \"digit\"; text: string; value: number }\n  | { kind: \"group\" | \"decimal\"; text: string }\n  | { kind: \"text\"; text: string };\n\nexport type NumberCell =\n  | { key: string; kind: \"digit\"; text: string; value: number }\n  | { key: string; kind: \"static\"; text: string };\n\nexport function localeKey(locales: Intl.LocalesArgument): string {\n  if (locales === undefined) return \"\";\n  return Array.isArray(locales) ? locales.join(\",\") : String(locales);\n}\n\nconst latinDigits: readonly string[] = [\n  \"0\",\n  \"1\",\n  \"2\",\n  \"3\",\n  \"4\",\n  \"5\",\n  \"6\",\n  \"7\",\n  \"8\",\n  \"9\",\n];\nconst glyphCache = new Map<string, readonly string[]>();\nconst decimalCache = new Map<string, string>();\n\n/**\n * The ten digits as the locale writes them, in order: \"0\" to \"9\" for most, the\n * Arabic-Indic digits for others. A wheel shows these glyphs.\n */\nexport function digitGlyphs(locales: Intl.LocalesArgument): readonly string[] {\n  const id = localeKey(locales);\n  let glyphs = glyphCache.get(id);\n  if (!glyphs) {\n    try {\n      const format = new Intl.NumberFormat(locales, { useGrouping: false });\n      glyphs = latinDigits.map((_, digit) => format.format(digit));\n    } catch {\n      glyphs = latinDigits;\n    }\n    glyphCache.set(id, glyphs);\n  }\n  return glyphs;\n}\n\n/** The decimal separator of the locale, which tells a preformatted string where its fraction starts. */\nexport function decimalSeparator(locales: Intl.LocalesArgument): string {\n  const id = localeKey(locales);\n  let separator = decimalCache.get(id);\n  if (!separator) {\n    try {\n      separator =\n        new Intl.NumberFormat(locales)\n          .formatToParts(1.1)\n          .find((part) => part.type === \"decimal\")?.value ?? \".\";\n    } catch {\n      separator = \".\";\n    }\n    decimalCache.set(id, separator);\n  }\n  return separator;\n}\n\n/** A number as Intl writes it, cut into digits, separators and the rest. */\nexport function numberTokens(\n  value: number,\n  locales: Intl.LocalesArgument,\n  format?: Intl.NumberFormatOptions,\n): NumberToken[] {\n  const glyphs = digitGlyphs(locales);\n  const tokens: NumberToken[] = [];\n  for (const part of new Intl.NumberFormat(locales, format).formatToParts(\n    value,\n  )) {\n    if (part.type === \"integer\" || part.type === \"fraction\") {\n      for (const char of part.value) {\n        const digit = glyphs.indexOf(char);\n        tokens.push(\n          digit >= 0\n            ? { kind: \"digit\", text: char, value: digit }\n            : { kind: \"text\", text: char },\n        );\n      }\n    } else if (part.type === \"group\" || part.type === \"decimal\") {\n      tokens.push({ kind: part.type, text: part.value });\n    } else {\n      tokens.push({ kind: \"text\", text: part.value });\n    }\n  }\n  return tokens;\n}\n\n// A number inside a string: digits, with group and decimal separators between\n// them. A space is a separator only when exactly three digits follow it, so\n// \"4 12\" stays two numbers.\nconst numericRun =\n  /[0-9]+(?:(?:[,.\\u00A0\\u202F'\\u2019]| (?=[0-9]{3}(?![0-9])))[0-9]+)*/g;\n// The rest of the string: a sign, a currency symbol or a percent sign alone, so\n// that each can come and go by itself, and any other text as one piece.\nconst textPiece =\n  /[+\\-\\u2212\\u00B1]|\\p{Sc}|[%\\u2030\\u2031]|[^+\\-\\u2212\\u00B1\\p{Sc}%\\u2030\\u2031]+/gu;\n\n/**\n * A string that is already formatted, cut the same way. Latin digits are\n * digits; the locale says which separator is the decimal one. A string without\n * digits is text only.\n */\nexport function stringTokens(\n  text: string,\n  locales: Intl.LocalesArgument,\n): NumberToken[] {\n  const decimal = decimalSeparator(locales);\n  const tokens: NumberToken[] = [];\n  const pushText = (segment: string) => {\n    for (const piece of segment.match(textPiece) ?? [])\n      tokens.push({ kind: \"text\", text: piece });\n  };\n  let cursor = 0;\n  for (const match of text.matchAll(numericRun)) {\n    pushText(text.slice(cursor, match.index));\n    const run = match[0];\n    const last = [...run].findLastIndex((char) => char < \"0\" || char > \"9\");\n    // The last separator is the decimal point when it is the locale's own and\n    // the only one of its kind; \"1.234.567\" has none.\n    const decimalAt =\n      last >= 0 && run[last] === decimal && run.split(decimal).length === 2\n        ? last\n        : -1;\n    [...run].forEach((char, index) => {\n      tokens.push(\n        char >= \"0\" && char <= \"9\"\n          ? { kind: \"digit\", text: char, value: Number(char) }\n          : { kind: index === decimalAt ? \"decimal\" : \"group\", text: char },\n      );\n    });\n    cursor = match.index + run.length;\n  }\n  pushText(text.slice(cursor));\n  return tokens;\n}\n\n/** The value a string stands for: its first number, negative after a minus sign. NaN without digits. */\nexport function tokensValue(tokens: readonly NumberToken[]): number {\n  const first = tokens.findIndex((token) => token.kind === \"digit\");\n  if (first < 0) return Number.NaN;\n  let digits = \"\";\n  for (const token of tokens.slice(first)) {\n    if (token.kind === \"digit\") digits += token.value;\n    else if (token.kind === \"decimal\") digits += \".\";\n    else if (token.kind === \"text\") break;\n  }\n  const negative = tokens\n    .slice(0, first)\n    .some(\n      (token) =>\n        token.kind === \"text\" &&\n        (token.text.includes(\"-\") || token.text.includes(\"\\u2212\")),\n    );\n  return Number(digits) * (negative ? -1 : 1);\n}\n\n/**\n * The cells of a number, each with the key of the place it holds. Digits are\n * keyed by their distance from the decimal point (units, tens, hundreds to the\n * left; tenths, hundredths to the right) and separators by the digits that\n * follow them, so \"9,990\" and \"10,000\" share every place but the new one. Text\n * is keyed by what it says and where it stands (before a number, after it), so\n * a \"%\" that stays is not a new \"%\" when a minus sign comes.\n */\nexport function numberCells(tokens: readonly NumberToken[]): NumberCell[] {\n  const cells: NumberCell[] = [];\n  const seen = new Map<string, number>();\n  let run = -1;\n  let gap = 0;\n  let index = 0;\n  while (index < tokens.length) {\n    const token = tokens[index]!;\n    if (token.kind === \"text\") {\n      const base = `t${gap}:${token.text}`;\n      const count = seen.get(base) ?? 0;\n      seen.set(base, count + 1);\n      cells.push({\n        key: count === 0 ? base : `${base}#${count}`,\n        kind: \"static\",\n        text: token.text,\n      });\n      index += 1;\n      continue;\n    }\n\n    let end = index;\n    while (end < tokens.length && tokens[end]!.kind !== \"text\") end += 1;\n    run += 1;\n    gap += 1;\n    const numeric = tokens.slice(index, end);\n    const point = numeric.findIndex((entry) => entry.kind === \"decimal\");\n    const integerEnd = point < 0 ? numeric.length : point;\n    let toRight = numeric\n      .slice(0, integerEnd)\n      .filter((entry) => entry.kind === \"digit\").length;\n    let fraction = 0;\n    numeric.forEach((entry, position) => {\n      if (position === point) {\n        cells.push({ key: `r${run}.d`, kind: \"static\", text: entry.text });\n      } else if (entry.kind === \"digit\") {\n        const key =\n          position < integerEnd\n            ? `r${run}.i${(toRight -= 1)}`\n            : `r${run}.f${fraction++}`;\n        cells.push({\n          key,\n          kind: \"digit\",\n          text: entry.text,\n          value: entry.value,\n        });\n      } else {\n        cells.push({\n          key:\n            position < integerEnd\n              ? `r${run}.g${toRight}`\n              : `r${run}.h${fraction}`,\n          kind: \"static\",\n          text: entry.text,\n        });\n      }\n    });\n    index = end;\n  }\n  return cells;\n}\n\n/** The direction a number moved in: 1 up, -1 down. Up when nothing is known or nothing moved. */\nexport function numberTrend(from: number, to: number): 1 | -1 {\n  return to < from ? -1 : 1;\n}\n\nexport const mod = (value: number, size: number) =>\n  ((value % size) + size) % size;\n\n/**\n * Where a wheel at `position` rolls to for `digit`: the nearest place beyond it\n * in the direction of the trend that shows the digit. The wheel is endless: 9 is\n * followed by 0, so a number that rises rolls every wheel upward, as an\n * odometer does. A wheel stopped between two digits (a change that arrives\n * while it still rolls) goes on from there.\n */\nexport function wheelTarget(\n  position: number,\n  digit: number,\n  trend: 1 | -1,\n): number {\n  if (trend > 0) {\n    const from = Math.ceil(position);\n    return from + mod(digit - from, 10);\n  }\n  const from = Math.floor(position);\n  return from - mod(from - digit, 10);\n}\n\n/** The places a wheel shows on its way: `count` rows from `base`; a row's digit is its place modulo 10. */\nexport function wheelRange(\n  from: number,\n  to: number,\n): { base: number; count: number } {\n  const base = Math.floor(Math.min(from, to));\n  return { base, count: Math.ceil(Math.max(from, to)) - base + 1 };\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-number-cells.ts"},{"path":"packages/ui/src/components/motion/motion-values.ts","content":"\"use client\";\n\nimport { animate } from \"motion/react\";\nimport * as React from \"react\";\n\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport { measureTiming, useLeavingEntries } from \"./motion-presence\";\n\n/**\n * Numbers that move toward new targets, for the geometry of a chart: the angles\n * of a ring, the points of a line. A component hands over what the data says now\n * and draws what it gets back; it writes no animation loop of its own. The\n * duration and the easing are the `--motion-duration-value` and\n * `--motion-ease-standard` tokens of the chart's own container, read when a run\n * starts (rulebook M1). Reduced motion (M5) shows every target at once, and the\n * first render shows its target, so the server and the client agree.\n */\n\n/** The values at `progress` of the way from `from` to `to` (0 is `from`, 1 is `to`). */\nexport function mixValues(\n  from: readonly number[],\n  to: readonly number[],\n  progress: number,\n): number[] {\n  return to.map((end, index) => {\n    const start = from[index] ?? end;\n    return start + (end - start) * progress;\n  });\n}\n\n// Two numbers that say the same. NaN is the same as NaN here: a target that\n// holds one is still the target the run heads for, and is not a new one on\n// every render.\nconst sameNumber = (a: number, b: number) =>\n  a === b || (Number.isNaN(a) && Number.isNaN(b));\n\n/** Whether two lists hold the same numbers in the same order, NaN matching NaN. */\nexport function sameValues(\n  a: readonly number[],\n  b: readonly number[],\n): boolean {\n  return (\n    a.length === b.length &&\n    a.every((value, index) => sameNumber(value, b[index]!))\n  );\n}\n\nfunction sameKeys(\n  a: readonly string[] | undefined,\n  b: readonly string[] | undefined,\n): boolean {\n  if (a === undefined || b === undefined) return a === b;\n  return a.length === b.length && a.every((key, index) => key === b[index]);\n}\n\n/**\n * Where the values shown before start in a list of values that is not the same\n * list any more. With keys, a value follows its key: a key that is new starts\n * at 0, so a new share grows out of nothing, or at the number `starts` names for\n * its place in the new list. Without keys the lists must be as long as each\n * other; otherwise there is no way to tell which is which and `null` says the\n * new values are shown at once.\n */\nexport function alignValues(\n  fromKeys: readonly string[] | undefined,\n  from: readonly number[],\n  toKeys: readonly string[] | undefined,\n  count: number,\n  starts?: readonly number[],\n): number[] | null {\n  if (fromKeys && toKeys) {\n    const byKey = new Map(fromKeys.map((key, index) => [key, from[index]!]));\n    return toKeys.map((key, index) => byKey.get(key) ?? starts?.[index] ?? 0);\n  }\n  return from.length === count ? [...from] : null;\n}\n\n/** One run of values toward a target. */\nexport type TweenRun = {\n  /** Where the run starts, aligned with `to`. */\n  from: readonly number[];\n  keys: readonly string[] | undefined;\n  /** Whether anything moves; false when the target is shown at once. */\n  moving: boolean;\n  to: readonly number[];\n};\n\n/** What a run shows at `progress`: its target at rest, the mix on its way. */\nexport function shownValues(\n  run: TweenRun,\n  progress: number,\n): readonly number[] {\n  return progress >= 1 || !run.moving\n    ? run.to\n    : mixValues(run.from, run.to, progress);\n}\n\n/**\n * The run for a new target, which starts from what is shown now so that a\n * target that changes on the way carries on from where the values are. `null`\n * when the target is the one the run already heads for.\n */\nexport function nextRun(\n  run: TweenRun,\n  progress: number,\n  target: readonly number[],\n  keys: readonly string[] | undefined,\n  reduced: boolean,\n  starts?: readonly number[],\n): TweenRun | null {\n  if (sameValues(target, run.to) && sameKeys(keys, run.keys)) return null;\n  const from = reduced\n    ? null\n    : alignValues(\n        run.keys,\n        shownValues(run, progress),\n        keys,\n        target.length,\n        starts,\n      );\n  return {\n    from: from ?? target,\n    keys,\n    moving: from !== null && !sameValues(from, target),\n    to: target,\n  };\n}\n\nexport type TweenOptions = {\n  /** The element whose motion tokens set the timing: the chart. The document root by default. */\n  container?: React.RefObject<Element | null>;\n  /**\n   * Names the values when their number changes, so each follows its own key:\n   * one that is new grows from 0. Without keys a change in the number of values\n   * shows the new values at once.\n   */\n  keys?: readonly string[];\n  /**\n   * Where each value starts whose key is new, in the order of the target. 0 for\n   * each by default, so a new value grows from nothing; a value that should\n   * appear where it is starts at its own target.\n   */\n  starts?: readonly number[];\n};\n\n/**\n * Moves the values toward their target when it changes, and returns what to\n * draw this frame. At rest it returns `target` itself (the same array), which\n * tells a component that nothing moves. A target equal in content to the last\n * one starts nothing, so a component may pass a fresh array on every render.\n */\nexport function useTweenedValues(\n  target: readonly number[],\n  { container, keys, starts }: TweenOptions = {},\n): readonly number[] {\n  const reduced = useReducedMotionPreference();\n  const [run, setRun] = React.useState<TweenRun>(() => ({\n    from: target,\n    keys,\n    moving: false,\n    to: target,\n  }));\n  const [progress, setProgress] = React.useState(1);\n\n  // A new target: start a run from what is shown now.\n  const next = nextRun(run, progress, target, keys, reduced, starts);\n  if (next) {\n    setRun(next);\n    setProgress(next.moving ? 0 : 1);\n  }\n\n  React.useLayoutEffect(() => {\n    if (!run.moving) return;\n    const controls = animate(0, 1, {\n      ...measureTiming(\n        container?.current ?? null,\n        \"--motion-duration-value\",\n        \"--motion-ease-standard\",\n      ),\n      onComplete: () => setProgress(1),\n      onUpdate: setProgress,\n    });\n    return () => controls.stop();\n  }, [container, run]);\n\n  return !run.moving || progress >= 1\n    ? target\n    : mixValues(run.from, run.to, progress);\n}\n\n/** A single number that moves toward its target; see `useTweenedValues`. */\nexport function useTweenedValue(\n  target: number,\n  options?: Pick<TweenOptions, \"container\">,\n): number {\n  const targets = React.useMemo(() => [target], [target]);\n  return useTweenedValues(targets, options)[0] ?? target;\n}\n\n/** One keyed row as it is drawn: its number so far, and whether it is on its way out. */\nexport type TweenedRow<T> = {\n  key: string;\n  leaving: boolean;\n  row: T;\n  value: number;\n};\n\n/** What a keyed row is without a number of its own: the entries as `useLeavingEntries` lists them. */\ntype RowEntry<T> = { element: { row: T }; leaving: boolean };\n\n/**\n * The numbers a list of rows heads for and the numbers its new rows start from.\n * A row that is there heads for the number `read` gives, one that is leaving for\n * the number it has while it is absent (`absent`, 0 by default), and a row that\n * is new starts from that same number.\n */\nexport function rowNumbers<T>(\n  entries: readonly RowEntry<T>[],\n  read: (row: T) => number,\n  absent?: (row: T) => number,\n): { starts: number[]; targets: number[] } {\n  const away = (entry: RowEntry<T>) => (absent ? absent(entry.element.row) : 0);\n  return {\n    starts: entries.map(away),\n    targets: entries.map((entry) =>\n      entry.leaving ? away(entry) : read(entry.element.row),\n    ),\n  };\n}\n\n/**\n * Moves the number of each keyed row toward the one `read` gives for the new\n * rows: a row that is new grows from 0, a row that is gone shrinks to 0 and\n * then leaves the list. `absent` gives another number for a row to start from\n * and to end at, for a number that is not a size: a row that should appear or\n * disappear where it stands has its own place for it, and one that stays glides\n * to its new place. The rows come back in order, a leaving row staying right\n * after the row it followed. Draw `value`; keep reading the data from the rows\n * you gave.\n */\nexport function useTweenedRows<T extends { id: string }>(\n  rows: readonly T[],\n  read: (row: T) => number,\n  {\n    absent,\n    ...options\n  }: Pick<TweenOptions, \"container\"> & {\n    /** The number a row has while it is not there: where a new row starts and where a leaving row ends. 0 by default. */\n    absent?: (row: T) => number;\n  } = {},\n): TweenedRow<T>[] {\n  const reduced = useReducedMotionPreference();\n  const present = rows.map((row) => ({ key: row.id, row }));\n  const { entries, remove } = useLeavingEntries(present, !reduced);\n  const keys = entries.map((entry) => String(entry.key));\n  const { starts, targets } = rowNumbers(entries, read, absent);\n  const values = useTweenedValues(targets, { ...options, keys, starts });\n\n  // The rows that left have shrunk to nothing: they go.\n  const settled = values === targets;\n  React.useEffect(() => {\n    if (!settled) return;\n    for (const entry of entries) if (entry.leaving) remove(entry.key);\n  });\n\n  return entries.map((entry, index) => ({\n    key: String(entry.key),\n    leaving: entry.leaving,\n    row: entry.element.row,\n    value: values[index] ?? 0,\n  }));\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-values.ts"},{"path":"packages/ui/src/components/motion/motion-scroll.ts","content":"\"use client\";\n\nimport { animate } from \"motion/react\";\n\nimport { getMotionTransitionFromCss } from \"../../system/motion/css-transition\";\n\n/** A scroll the motion component runs; `stop` ends it where it is. */\nexport type ScrollMotion = { stop: () => void };\n\n/** Reduced motion scrolls at once (rulebook M5): the document mode, the provider preset and the system setting. */\nfunction scrollsAtOnce(element: HTMLElement): boolean {\n  return (\n    document.documentElement.dataset.motionReduce === \"true\" ||\n    Boolean(element.closest('[data-sp-motion=\"reduced\"]')) ||\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\n/**\n * Scrolls an element along the inline axis to `left`, its physical\n * scrollLeft, which runs toward negative values in a right-to-left element. It\n * takes the tokens of a list that scrolls to a target (rulebook M1), read when\n * the scroll starts, so a token set on any container around the element\n * applies. With `smooth` false, or under reduced motion, it jumps there at\n * once and returns null.\n */\nexport function scrollInlineTo(\n  element: HTMLElement,\n  left: number,\n  smooth: boolean,\n): ScrollMotion | null {\n  if (!smooth || scrollsAtOnce(element)) {\n    element.scrollLeft = left;\n    return null;\n  }\n  return animate(element.scrollLeft, left, {\n    ...getMotionTransitionFromCss(\n      element,\n      \"--motion-duration-moderate\",\n      \"--motion-ease-standard\",\n    ),\n    onUpdate: (value) => {\n      element.scrollLeft = value;\n    },\n  });\n}\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-scroll.ts"},{"path":"packages/ui/src/components/motion/motion-follow-value.ts","content":"\"use client\";\n\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  type MotionValue,\n} from \"motion/react\";\nimport * as React from \"react\";\n\nimport { useReducedMotionPreference } from \"../../system/motion/use-reduced-motion-preference\";\nimport { measureTiming } from \"./motion-presence\";\n\n/**\n * Something that follows a target: the crosshair of a chart that glides to the\n * next item, the card of a tooltip that trails it, a pill under the axis. A\n * component hands over where the target is now and binds what it gets back to\n * an element; it writes no animation loop of its own. The glide is a spring\n * that never overshoots, timed by a `--motion-duration-*` token of the chart's\n * own container, read when a glide starts (rulebook M1). Reduced motion (M5)\n * puts every follower at its target at once, and the first render shows the\n * target, so the server and the client agree.\n */\n\n/** The token the pointer's followers take: the crosshair, the dots and the pill of a chart. */\nexport const FOLLOW_DURATION = \"--motion-duration-follow\";\n/** The token a card takes that trails them, so it visibly lags the item it belongs to. */\nexport const FOLLOW_TRAIL_DURATION = \"--motion-duration-follow-trail\";\n\n/** A spring that Motion's `animate` takes: stiffness and damping instead of a duration, so a value that is sent to a new target on the way keeps its speed. */\nexport type FollowSpring = {\n  damping: number;\n  mass: 1;\n  restDelta: number;\n  restSpeed: number;\n  stiffness: number;\n  type: \"spring\";\n};\n\n/**\n * The spring that brings a value to its target in `seconds` without bouncing:\n * critically damped, with the frequency Motion's own `visualDuration` gives a\n * spring of that duration (`2π / 1.2 seconds`), so the value is at 97 % of the\n * way after `seconds` and sits on the target soon after. Motion's duration-based\n * springs forget the speed of a value that is retargeted on the way; these keep\n * it, and a pointer that sweeps across items moves the follower on without\n * stopping at each of them.\n */\nexport function followSpring(seconds: number): FollowSpring {\n  const frequency = (2 * Math.PI) / (1.2 * seconds);\n  return {\n    damping: 2 * frequency,\n    mass: 1,\n    // A hundredth of a pixel and a tenth of a pixel a second: still for the eye.\n    restDelta: 0.01,\n    restSpeed: 0.1,\n    stiffness: frequency * frequency,\n    type: \"spring\",\n  };\n}\n\n/** How a change of the target is shown. */\nexport type FollowMode = \"glide\" | \"jump\";\n\n/**\n * A follower glides only while it is active and was active before: one that\n * comes in is put where its target is, instead of gliding from where it was\n * last seen. It also jumps when motion is reduced and when the token gives it\n * no time.\n */\nexport function followMode({\n  active,\n  reduced,\n  seconds,\n  wasActive,\n}: {\n  active: boolean;\n  reduced: boolean;\n  seconds: number;\n  wasActive: boolean;\n}): FollowMode {\n  return active && wasActive && !reduced && seconds > 0 ? \"glide\" : \"jump\";\n}\n\n/**\n * Sends a follower to its target: puts the value there at once (`jump`), or\n * glides it there and returns what stops the glide. A glide that has nothing to\n * cover is not started.\n */\nexport function startFollow(\n  value: MotionValue<number>,\n  target: number,\n  mode: FollowMode,\n  seconds: number,\n): (() => void) | undefined {\n  if (mode === \"jump\") {\n    value.jump(target);\n    return undefined;\n  }\n  if (value.get() === target) return undefined;\n  const controls = animate(value, target, followSpring(seconds));\n  return () => controls.stop();\n}\n\nexport type FollowValueOptions = {\n  /** The element whose motion tokens set the timing: the chart. The document root by default. */\n  container?: React.RefObject<Element | null> | undefined;\n  /** The token that sets how long a glide takes: `FOLLOW_DURATION` by default. */\n  duration?: `--motion-duration-${string}` | undefined;\n  /**\n   * Whether the follower is in use. One that is not sits at its target; when it\n   * becomes active it is already in its place, and glides from there on. A part\n   * that fades out waits where it was and stays inactive until it is needed.\n   */\n  active?: boolean | undefined;\n};\n\n/**\n * A number that glides to its target when the target changes, as a motion value\n * to bind to an element (`followed.line`, `followed.circle`, ...): the element\n * moves frame by frame and the component does not render again. At rest the\n * value is the target itself.\n */\nexport function useFollowValue(\n  target: number,\n  {\n    active = true,\n    container,\n    duration = FOLLOW_DURATION,\n  }: FollowValueOptions = {},\n): MotionValue<number> {\n  const reduced = useReducedMotionPreference();\n  const value = useMotionValue(target);\n  const wasActive = React.useRef(active);\n\n  React.useLayoutEffect(() => {\n    const seconds =\n      (\n        measureTiming(\n          container?.current ?? null,\n          duration,\n          \"--motion-ease-standard\",\n        ) as { duration?: number }\n      ).duration ?? 0;\n    const mode = followMode({\n      active,\n      reduced,\n      seconds,\n      wasActive: wasActive.current,\n    });\n    wasActive.current = active;\n    return startFollow(value, target, mode, seconds);\n  }, [active, container, duration, reduced, target, value]);\n\n  return value;\n}\n\n/**\n * Keeps `target` at what `derive` makes of `source`, now and every time `source`\n * moves, and returns what ends it. `derive` is read again at each move, so one\n * that changes with the component is always the latest.\n */\nexport function bindDerived(\n  source: MotionValue<number>,\n  target: MotionValue<number>,\n  derive: () => (value: number) => number,\n): () => void {\n  const follow = (value: number) => target.set(derive()(value));\n  follow(source.get());\n  return source.on(\"change\", follow);\n}\n\n/**\n * A motion value that is a function of another and moves with it: a mark that\n * rides a curve while it glides along it, whose height is the curve's at the\n * place it has reached. It starts at what `derive` makes of the source.\n */\nexport function useFollowDerived(\n  source: MotionValue<number>,\n  derive: (value: number) => number,\n): MotionValue<number> {\n  const derived = useMotionValue(derive(source.get()));\n  const latest = React.useRef(derive);\n  latest.current = derive;\n  React.useLayoutEffect(\n    () => bindDerived(source, derived, () => latest.current),\n    [derived, source],\n  );\n  // A `derive` that changes while the source stands still moves the value too.\n  React.useLayoutEffect(() => {\n    derived.set(derive(source.get()));\n  });\n  return derived;\n}\n\n/** Two numbers that glide to the place of a point; see `useFollowValue`. */\nexport function useFollowPoint(\n  target: { x: number; y: number },\n  options?: FollowValueOptions,\n): { x: MotionValue<number>; y: MotionValue<number> } {\n  return {\n    x: useFollowValue(target.x, options),\n    y: useFollowValue(target.y, options),\n  };\n}\n\n/**\n * Renders the component again at every frame in which one of the values moves.\n * For a part that has to hand the numbers on as props, such as the anchor of a\n * floating element; anything that can bind the motion values to an element\n * should do that instead, and not render.\n */\nexport function useFollowFrames(values: readonly MotionValue<number>[]): void {\n  const [, render] = React.useReducer((count: number) => count + 1, 0);\n  React.useEffect(() => {\n    const stops = values.map((value) => value.on(\"change\", render));\n    return () => {\n      for (const stop of stops) stop();\n    };\n    // The motion values are the same ones for as long as the component lives.\n  }, values);\n}\n\n/**\n * The elements that follow a motion value from `useFollowValue`: an attribute\n * or the style of one takes the value, and the element moves with it. A path\n * has no position attribute, so it moves through its style: `style={{ x, y }}`\n * translates it.\n */\nexport const followed = {\n  circle: motion.circle,\n  div: motion.div,\n  line: motion.line,\n  path: motion.path,\n  rect: motion.rect,\n} as const;\n","type":"registry:ui","target":"~/src/siteplane/components/motion/motion-follow-value.ts"},{"path":"packages/ui/src/components/motion/index.ts","content":"export * from \"./motion\";\nexport {\n  FOLLOW_DURATION,\n  FOLLOW_TRAIL_DURATION,\n  followMode,\n  followSpring,\n  followed,\n  useFollowDerived,\n  useFollowFrames,\n  useFollowPoint,\n  useFollowValue,\n  type FollowMode,\n  type FollowSpring,\n  type FollowValueOptions,\n} from \"./motion-follow-value\";\nexport { MotionNumber, type MotionNumberProps } from \"./motion-number\";\nexport {\n  useTweenedRows,\n  useTweenedValue,\n  useTweenedValues,\n  type TweenedRow,\n  type TweenOptions,\n} from \"./motion-values\";\n","type":"registry:ui","target":"~/src/siteplane/components/motion/index.ts"},{"path":"packages/ui/src/system/motion/css-transition.ts","content":"function parseCubicBezier(\n  value: string,\n): [number, number, number, number] | undefined {\n  const match = value.match(\n    /^cubic-bezier\\(([-\\d.]+),\\s*([-\\d.]+),\\s*([-\\d.]+),\\s*([-\\d.]+)\\)$/,\n  );\n  return match\n    ? [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])]\n    : undefined;\n}\nexport function getMotionTransitionFromCss(\n  element: Element,\n  durationVariable: `--motion-${string}`,\n  easingVariable: `--motion-${string}`,\n) {\n  const styles = getComputedStyle(element);\n  const duration = styles.getPropertyValue(durationVariable).trim();\n  const easing = styles.getPropertyValue(easingVariable).trim();\n  return {\n    duration:\n      (Number.parseFloat(duration) || 0) / (duration.endsWith(\"ms\") ? 1000 : 1),\n    ease:\n      easing === \"linear\"\n        ? (\"linear\" as const)\n        : (parseCubicBezier(easing) ?? [0.16, 1, 0.3, 1]),\n  };\n}\n","type":"registry:lib","target":"~/src/siteplane/system/motion/css-transition.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/motion.md","categories":["Layout & utilities"],"meta":{"avoidWhen":["Avoid for one element that already animates itself, such as a Collapsible, a Dialog or a Toast.","Avoid for lists of hundreds of rows: every item is a Motion element. Virtualize the list instead."],"description":"Animate items that appear, leave, move or replace one another, and numbers that change, with no animation code.","knownLimitations":["Items need a stable key. An index as the key makes a removed item leave as its neighbor.","In a stack, set the space between items with the gap prop, which adds space between items only and none above the first or below the last. A CSS gap on the list leaves a hole until the item has left.","Items must be direct children of the list, not wrapped in a fragment: a fragment around several items counts as one.","A flow list moves the height of its own box when a row of items appears or goes. A list that is display: contents has no box, so pass the element that wraps its items as container.","While a stack item, a reveal or a swap changes, its own element is the clipping box, so give that element no padding or border and put them on the content inside it.","A leaving item is inert, so focus inside it is lost when it leaves. Move focus before removing it.","MotionNumber formats a number in en-US unless you pass locales, so the server and the client always agree. A string is shown as it is and only its Latin digits (0 to 9) roll; every other character fades.","MotionNumber is read once, as its final text, with no live announcement. Wrap it in your own live region when a change must be announced.","The value tweens move lists of numbers of the same length. A list that changes its length shows the new values at once, unless the values are keyed with useTweenedRows."],"providerNotes":["Depends on the motion package, which the registry installs with the component.","SiteplaneMotionProvider preset=\"reduced\" and the system setting make every part instant."],"releaseStatus":"stable","searchTerms":["motion","animation","presence","animate presence","transition","list","reorder","reveal","swap","enter","exit","number","rolling number","odometer","counter","tween","chart"],"useWhen":["Use when items are added, removed or reordered and the items around them should glide into their new place.","Use when a message, an error or a bar appears and disappears and should not push the layout.","Use when a step, a state or a panel replaces another, so that the old content fades out before the new content fades in and the container follows the new height.","Use when a figure changes in place, such as a total, a rate or a count, and its digits should roll to the new value.","Use when the marks of a chart should glide to new data: the value tweens move numbers toward their new targets.","Use when a mark or a card should glide after the pointer from item to item without overshooting, as the crosshair and the tooltip of a chart do."]}}