{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion",
  "type": "registry:lib",
  "title": "seam motion tokens",
  "description": "Spring presets and the depth (z-axis) scale that power every seamui animation.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/seam/lib/motion.ts",
      "content": "// seamui motion tokens — the single source of truth for all animation.\n// Springs over durations; depth over flatness. See seamui docs → Motion.\nimport * as React from \"react\"\nimport {\n  animate,\n  MotionConfigContext,\n  useReducedMotion as useSystemReducedMotion,\n} from \"motion/react\"\nimport type { TargetAndTransition, Transition } from \"motion/react\"\n\n/**\n * How the app wants motion resolved. `system` (the default) follows the OS.\n */\nexport type MotionPreference = \"system\" | \"reduce\" | \"full\"\n\nconst MotionPreferenceContext = React.createContext<MotionPreference>(\"system\")\n\n/**\n * Force the reduced (or full) variant for a subtree, overriding the OS.\n *\n *   <MotionPreferenceProvider preference=\"reduce\">\n *\n * Without one, everything follows `prefers-reduced-motion`, which is what an\n * app should normally do — this exists so a settings screen (or the docs\n * playground) can offer motion as a user-facing choice.\n */\nfunction MotionPreferenceProvider({\n  preference,\n  children,\n}: {\n  preference: MotionPreference\n  children: React.ReactNode\n}) {\n  return React.createElement(\n    MotionPreferenceContext.Provider,\n    { value: preference },\n    children\n  )\n}\n\n/**\n * The reduced-motion signal every seamui component branches on (§5b).\n *\n * Import this from `@/lib/motion`, never `useReducedMotion` from motion/react\n * — going through here is what lets an app override the OS.\n *\n * Resolution order:\n *   1. a surrounding `<MotionPreferenceProvider>` — an explicit app choice;\n *   2. `<MotionConfig reducedMotion=\"always\">` — unambiguous opt-in, honored\n *      so motion's own API keeps working;\n *   3. the OS `prefers-reduced-motion` media query.\n *\n * Note what is deliberately NOT honored: `MotionConfig`'s `reducedMotion:\n * \"never\"`. That is the *default* value of motion's context (see\n * MotionConfigContext), so it appears even when no provider exists and cannot\n * be told apart from a real opt-out. Treating it as \"force full motion\" is\n * exactly how this hook once silently ignored the OS setting for every\n * consumer — the §5b failure with its polarity flipped.\n */\nfunction useReducedMotion(): boolean {\n  const preference = React.useContext(MotionPreferenceContext)\n  const { reducedMotion } = React.useContext(MotionConfigContext)\n  const system = useSystemReducedMotion()\n\n  if (preference === \"reduce\" || reducedMotion === \"always\") return true\n  if (preference === \"full\") return false\n  return system ?? false\n}\n\nexport { MotionPreferenceProvider, useReducedMotion }\n\n/**\n * True only after the first client render. Gate a motion `initial` that moves\n * (scale/translate) behind this so SSR and the client's first paint agree —\n * motion serializes an animated `initial` transform on the server that the\n * client's hydration doesn't, which trips React's hydration-mismatch warning.\n * Elements that mount *after* hydration (a new chat message, an added chip)\n * still get their entrance, since `initial` applies on mount.\n *\n *   const mounted = useMounted()\n *   initial={mounted ? (reduceMotion ? reduced.fadeIn.initial : depth.overlay.initial) : false}\n *\n * Opacity-only `initial` (e.g. `{ opacity: 0 }`) doesn't need this — no\n * transform is serialized, so server and client already agree.\n */\nexport function useMounted(): boolean {\n  const [mounted, setMounted] = React.useState(false)\n  React.useEffect(() => setMounted(true), [])\n  return mounted\n}\n\n/**\n * ── Personality: retune the whole library in one line ────────────────\n * Every seamui animation pulls its spring from `springs`, and `springs`\n * just picks a personality below. Swap the pick — or edit the numbers —\n * and all components change feel together; no component files to touch.\n *\n * Each personality defines the same four roles:\n *   press   — press-down feedback: near-instant\n *   snappy  — release / settle / state changes\n *   surface — overlays entering (dialogs, popovers, sheets)\n *   bouncy  — playful accents (toasts, badges); use sparingly\n */\nexport const personalities = {\n  /** The seam default — quick and physical, with a hint of life. */\n  seam: {\n    press: { type: \"spring\", stiffness: 600, damping: 40, mass: 0.5 },\n    snappy: { type: \"spring\", stiffness: 420, damping: 30, mass: 0.7 },\n    surface: { type: \"spring\", stiffness: 320, damping: 28, mass: 0.9 },\n    bouncy: { type: \"spring\", stiffness: 380, damping: 18, mass: 0.9 },\n  },\n  /** Tighter and faster, no overshoot — dense professional tools. */\n  brisk: {\n    press: { type: \"spring\", stiffness: 800, damping: 50, mass: 0.4 },\n    snappy: { type: \"spring\", stiffness: 560, damping: 40, mass: 0.55 },\n    surface: { type: \"spring\", stiffness: 440, damping: 38, mass: 0.7 },\n    bouncy: { type: \"spring\", stiffness: 500, damping: 26, mass: 0.7 },\n  },\n  /** Softer and slower — calm, editorial surfaces. */\n  relaxed: {\n    press: { type: \"spring\", stiffness: 420, damping: 36, mass: 0.7 },\n    snappy: { type: \"spring\", stiffness: 280, damping: 28, mass: 0.9 },\n    surface: { type: \"spring\", stiffness: 220, damping: 26, mass: 1.1 },\n    bouncy: { type: \"spring\", stiffness: 260, damping: 18, mass: 1 },\n  },\n  /** More overshoot everywhere — playful, consumer-facing apps. */\n  playful: {\n    press: { type: \"spring\", stiffness: 620, damping: 30, mass: 0.5 },\n    snappy: { type: \"spring\", stiffness: 420, damping: 20, mass: 0.8 },\n    surface: { type: \"spring\", stiffness: 340, damping: 18, mass: 0.9 },\n    bouncy: { type: \"spring\", stiffness: 400, damping: 12, mass: 1 },\n  },\n} as const satisfies Record<\n  string,\n  Record<\"press\" | \"snappy\" | \"surface\" | \"bouncy\", Transition>\n>\n\n/** Spring presets, tuned against 60fps mobile feel. Pick a personality here. */\nexport const springs = personalities.seam\n\n/** Opacity-only fades (the one place plain durations are allowed). */\nexport const fades = {\n  fast: { duration: 0.12, ease: \"easeOut\" },\n  normal: { duration: 0.2, ease: \"easeOut\" },\n} as const satisfies Record<string, Transition>\n\n/**\n * Depth scale — virtual z-axis positions expressed as scale + shadow pairs.\n * pressed  : element pushed into the surface\n * resting  : neutral\n * raised   : hover/lifted state\n * overlay  : floating surfaces rising with overlay depth\n * modal    : top-of-stack surfaces\n *\n * overlay/modal are for elements **motion.dev controls end to end** — list\n * entries, chips, a scroll-to-bottom button (AnimatePresence owns their\n * mount/unmount, so `exit` runs). Base UI popups do NOT use these: Base UI\n * owns their lifecycle and awaits CSS—not motion's rAF springs—before\n * unmounting, so those use `condense` below instead.\n */\nexport const depth = {\n  pressed: { scale: 0.97 },\n  resting: { scale: 1 },\n  raised: { scale: 1.02 },\n  overlay: {\n    initial: { opacity: 0, scale: 0.96, y: 4 },\n    animate: { opacity: 1, scale: 1, y: 0 },\n    exit: { opacity: 0, scale: 0.98, y: 2 },\n  },\n  modal: {\n    initial: { opacity: 0, scale: 0.96, y: 8 },\n    animate: { opacity: 1, scale: 1, y: 0 },\n    exit: { opacity: 0, scale: 0.97, y: 6 },\n  },\n} as const\n\n/**\n * Drill-down — moving between levels of a single surface (the nested dropdown\n * menu). Unlike `depth`, nothing changes z-position: the level you're entering\n * slides in laterally from the direction of travel while the surface springs to\n * its new size around it. `direction` is 1 drilling in, -1 stepping back.\n *\n * Only the level being entered animates. Its predecessor is unmounted the\n * instant the path changes — a menu is a roving-focus composite, and a level\n * that lingers to animate out is a level whose items are still registered for\n * arrow keys. Movement here is deliberately short: the size spring carries the\n * transition, the slide only says which way you went.\n *\n * Reduced motion drops the travel and keeps the fade — use `reduced.fadeIn`.\n */\nexport const drill = {\n  /** Entry offset for the incoming level, in px, signed by travel direction. */\n  enter: (direction: 1 | -1): TargetAndTransition => ({\n    opacity: 0,\n    x: direction * 14,\n  }),\n  /** Settled: flush with the surface. */\n  settle: { opacity: 1, x: 0 } satisfies TargetAndTransition,\n} as const\n\n/**\n * The seam \"condense\" — how every Base UI overlay animates: rise + fade in,\n * fall back + fade out, backdrop dimming on the same clock. In CSS (keyed to\n * Base UI's `data-starting-style` / `data-ending-style`) because Base UI keeps\n * a popup mounted through its exit and awaits CSS transitions before\n * unmounting — it can't await motion's rAF springs, which is why exits used to\n * cut instantly. Scale rides the standalone `scale` property (Base UI owns\n * `transform` for positioning, so a transform-based scale would be clobbered);\n * a spring-shaped bezier keeps the seam bounce. The one place seam expresses\n * motion as classes — because Base UI's lifecycle is CSS-native.\n */\nexport const condense = {\n  /** Popup surfaces: rise + fade from the trigger, fall back quicker on exit.\n   *  Scale originates from Base UI's `--transform-origin` (the trigger side),\n   *  so overlay-depth popups grow toward the user out of their anchor. */\n  surface:\n    \"origin-[var(--transform-origin)] transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.22,1.3,0.36,1)] data-[starting-style]:opacity-0 data-[starting-style]:[scale:0.95] data-[ending-style]:opacity-0 data-[ending-style]:[scale:0.96] data-[ending-style]:duration-150 data-[ending-style]:ease-out motion-reduce:transition-opacity motion-reduce:data-[starting-style]:[scale:1] motion-reduce:data-[ending-style]:[scale:1]\",\n  /** Backdrops / scrims: same clock as the panel, no transform. */\n  backdrop:\n    \"transition-opacity duration-200 ease-out data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 data-[ending-style]:duration-150\",\n  /** Bottom sheet: slides up from off-screen and fades in, falls back down on\n   *  dismiss (Base UI awaits it). The slide rides the standalone `translate`\n   *  property because Base UI owns `transform` for the swipe — keyed to\n   *  `data-starting-style`/`data-ending-style`, and self-suppressed mid-drag so\n   *  the gesture stays 1:1. */\n  sheet:\n    \"transition-[translate,opacity] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[starting-style]:translate-y-full data-[starting-style]:opacity-0 data-[ending-style]:translate-y-full data-[ending-style]:opacity-0 data-[dragging]:transition-none motion-reduce:transition-opacity motion-reduce:data-[starting-style]:translate-y-0 motion-reduce:data-[ending-style]:translate-y-0\",\n  /** Toast: rises + fades in with the seam bounce, falls back + fades quicker\n   *  on dismiss. Base UI owns the stacking `transform` and swipe, so the\n   *  entrance/exit offset rides `transform` here alongside opacity; the swipe\n   *  exit self-cancels the vertical fall so the gesture direction wins. */\n  toast:\n    \"[transition:transform_0.5s,opacity_0.35s] ease-[cubic-bezier(0.22,1.3,0.36,1)] data-[starting-style]:translate-y-6 data-[starting-style]:opacity-0 data-[ending-style]:translate-y-4 data-[ending-style]:opacity-0 data-[ending-style]:[&[data-swipe-direction]]:translate-y-0 motion-reduce:[transition:opacity_0.35s] motion-reduce:data-[starting-style]:translate-y-0 motion-reduce:data-[ending-style]:translate-y-0\",\n} as const\n\n/**\n * Error feedback — a brief horizontal shake. A keyframe sequence a spring\n * can't express, so it carries its own duration (like fades). Movement, so\n * it never runs under reduced motion — pair with `reduced.flash`.\n */\nexport const shake: { animate: TargetAndTransition; transition: Transition } = {\n  animate: { x: [0, -6, 6, -4, 4, 0] },\n  transition: { duration: 0.32, ease: \"easeInOut\" },\n}\n\ntype PressableProps = {\n  onPointerDown?: React.PointerEventHandler<HTMLElement>\n  onPointerUp?: React.PointerEventHandler<HTMLElement>\n  onPointerCancel?: React.PointerEventHandler<HTMLElement>\n  onPointerLeave?: React.PointerEventHandler<HTMLElement>\n  onKeyDown?: React.KeyboardEventHandler<HTMLElement>\n  onKeyUp?: React.KeyboardEventHandler<HTMLElement>\n}\n\n/**\n * Imperative press depth for controls whose rendered element must stay a\n * plain DOM node. Base UI composite widgets (Toolbar, Toggle Group, Tabs,\n * Menubar) register items and rove focus through the element's ref, and a\n * motion component in that render path breaks the registration — even\n * `render={<motion.button/>}` leaves arrow-key navigation dead. So\n * composite items press via motion's imperative `animate()` on the plain\n * element instead (same tokens, same feel).\n *\n * Returns a props merger: wrap the (Base UI-provided) render props and the\n * element presses into the surface on pointer/keyboard activation and\n * settles springy on release. Reduced motion dims instead of moving (§5b).\n *\n *   const withPress = usePressDepth(disabled)\n *   render={(props) => <button {...withPress(props)} />}\n */\nexport function usePressDepth(disabled = false) {\n  const reduceMotion = useReducedMotion() ?? false\n\n  return React.useCallback(\n    <P extends PressableProps>(props: P): P => {\n      const press = (el: HTMLElement) => {\n        if (disabled) return\n        if (reduceMotion) animate(el, reduced.pressed, fades.fast)\n        else animate(el, depth.pressed, springs.press)\n      }\n      const settle = (el: HTMLElement) => {\n        if (disabled) return\n        if (reduceMotion) animate(el, { opacity: 1 }, fades.fast)\n        else animate(el, depth.resting, springs.snappy)\n      }\n      return {\n        ...props,\n        onPointerDown: (e) => {\n          props.onPointerDown?.(e)\n          if (e.button === 0) press(e.currentTarget)\n        },\n        onPointerUp: (e) => {\n          props.onPointerUp?.(e)\n          settle(e.currentTarget)\n        },\n        onPointerCancel: (e) => {\n          props.onPointerCancel?.(e)\n          settle(e.currentTarget)\n        },\n        onPointerLeave: (e) => {\n          props.onPointerLeave?.(e)\n          settle(e.currentTarget)\n        },\n        // Feedback must fire on keyboard activation too (§1).\n        onKeyDown: (e) => {\n          props.onKeyDown?.(e)\n          if (!e.repeat && (e.key === \" \" || e.key === \"Enter\"))\n            press(e.currentTarget)\n        },\n        onKeyUp: (e) => {\n          props.onKeyUp?.(e)\n          if (e.key === \" \" || e.key === \"Enter\") settle(e.currentTarget)\n        },\n      }\n    },\n    [disabled, reduceMotion]\n  )\n}\n\n/**\n * Reduced-motion fallbacks — used when `useReducedMotion()` is true.\n * Policy: never go dead. Swap movement (scale/translate) for opacity so\n * every interaction still gives feedback; it just doesn't travel.\n */\nexport const reduced = {\n  /** Press feedback without movement: a brief dim. */\n  pressed: { opacity: 0.7 },\n  /** Entrances collapse to opacity-only fades. */\n  fadeIn: {\n    initial: { opacity: 0 },\n    animate: { opacity: 1 },\n    exit: { opacity: 0 },\n  },\n  /** Layout / position changes jump instantly instead of springing. */\n  instant: { duration: 0 } satisfies Transition,\n  /** Error/attention feedback without movement: a brief opacity pulse. */\n  flash: {\n    animate: { opacity: [1, 0.45, 1] },\n    transition: { duration: 0.32, ease: \"easeInOut\" },\n  } as { animate: TargetAndTransition; transition: Transition },\n} as const\n",
      "type": "registry:lib",
      "target": "lib/motion.ts"
    }
  ]
}