{
  "name": "motion-primitive",
  "type": "registry:lib",
  "description": "Enter/exit motion contract shared by the animation primitives — opacity leads, exits are shorter, and a bounded Sequence times the exit itself",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "springs",
    "timing"
  ],
  "files": [
    {
      "path": "registry/bases/default/lib/motion-primitive.ts",
      "type": "registry:lib",
      "content": "import type { CSSProperties, ReactNode } from \"react\";\nimport {\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n  type SpringConfig,\n} from \"remotion\";\nimport { springBouncy, springSmooth, springSnappy } from \"./springs\";\nimport { EASING_ENTER, EASING_EXIT } from \"./timing\";\n\n/**\n * One motion contract behind every enter/exit primitive.\n *\n * Two rules do most of the work:\n *\n * 1. **Light leads the move.** Opacity finishes at ~55% of an entrance, so the\n *    last third of the travel happens on a fully opaque element. Cross-fading\n *    for the whole move is what makes an animation read as a dissolve rather\n *    than an arrival.\n * 2. **An exit is not an entrance played backwards.** It is shorter (70% of the\n *    enter duration), it travels less (60% of the distance), and it accelerates\n *    away instead of decelerating into place.\n *\n * Exits are also automatic: inside a `<Sequence durationInFrames={n}>`,\n * `useVideoConfig().durationInFrames` is that window, so `exit` alone is enough\n * to land the element out exactly at the end of its slot.\n */\n\nexport type SpringPreset = \"smooth\" | \"snappy\" | \"bouncy\";\n\n/** `true` picks the snappy preset; an object overrides fields on it. */\nexport type MotionSpring = boolean | SpringPreset | Partial<SpringConfig>;\n\nconst SPRING_PRESETS: Record<SpringPreset, SpringConfig> = {\n  smooth: springSmooth,\n  snappy: springSnappy,\n  bouncy: springBouncy,\n};\n\nexport function resolveSpringConfig(value: MotionSpring): SpringConfig {\n  if (value === true || value === false) return springSnappy;\n  if (typeof value === \"string\") return SPRING_PRESETS[value] ?? springSnappy;\n  return { ...springSnappy, ...value };\n}\n\n/**\n * Light snaps, geometry glides.\n *\n * `EASING_ENTER` is an expo-out: it is ~90% resolved a third of the way in,\n * which is right for opacity but throws away most of a transform — a hinge or\n * a defocus is over before the element is even visible. The transform channel\n * decelerates on a cubic instead, so the stated duration is the duration the\n * eye actually sees.\n */\nconst EASING_ARRIVE = Easing.bezier(0.33, 1, 0.68, 1);\n\n/** Opacity is complete this far into an entrance. */\nconst ENTER_OPACITY_LEAD = 0.55;\n/** Opacity is gone this far into an exit — the tail of the travel is unseen. */\nconst EXIT_OPACITY_LEAD = 0.7;\n/** Exit duration as a share of the enter duration, when not given. */\nconst EXIT_DURATION_RATIO = 0.7;\nconst MIN_EXIT_FRAMES = 6;\n/** Exit travel as a share of the enter distance. */\nconst DEFAULT_EXIT_TRAVEL = 0.6;\n\nexport const DEFAULT_ENTER_FRAMES = 30;\n\n/** Exit length a primitive uses when it is not given one. */\nexport function defaultExitFrames(durationInFrames: number): number {\n  return Math.max(\n    MIN_EXIT_FRAMES,\n    Math.round(durationInFrames * EXIT_DURATION_RATIO),\n  );\n}\n\nexport type EnterExitOptions = {\n  /** Length of the entrance in frames. */\n  durationInFrames?: number;\n  /** Frames to wait before the entrance starts. */\n  delayInFrames?: number;\n  /** Drive the entrance with a spring instead of the ease-out curve. */\n  spring?: MotionSpring;\n  /** Animate back out. Lands on the last frame of the surrounding Sequence. */\n  exit?: boolean;\n  /** Length of the exit. Defaults to 70% of `durationInFrames`. */\n  exitInFrames?: number;\n  /** Frame the exit starts on. Overrides the automatic end-of-window timing. */\n  exitAtInFrames?: number;\n  /** Share of the enter distance the exit travels. */\n  exitTravel?: number;\n  /** `reverse` leaves the way it came in, `continue` carries on through. */\n  exitDirection?: \"reverse\" | \"continue\";\n};\n\n/** Props every wrapper primitive accepts on top of its own options. */\nexport type MotionPrimitiveProps = EnterExitOptions & {\n  children: ReactNode;\n  /** Fill the parent's width instead of shrink-wrapping the child. */\n  block?: boolean;\n  style?: CSSProperties;\n  className?: string;\n};\n\nexport type MotionState = {\n  /** 0 at the start, 1 at rest, back to 0 on the way out. May overshoot 1. */\n  motion: number;\n  /** How far from rest the element still is — multiply distances by this. */\n  displace: number;\n  /** Opacity channel. Leads the transform in and out. */\n  opacity: number;\n  /** Direction multiplier for the displacement, -1 while exiting forwards. */\n  sign: 1 | -1;\n  /** True from the first frame of the exit onwards. */\n  exiting: boolean;\n  /** Eased 0→1 across the exit window. */\n  exitProgress: number;\n  /** Linear 0→1 across the entrance window, before easing. */\n  enterTime: number;\n};\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * Resolves the enter and exit channels for one element.\n * Every wrapper primitive in the registry is a different way of spending the\n * `motion`/`displace`/`opacity` values this returns.\n */\nexport function useEnterExit({\n  durationInFrames = DEFAULT_ENTER_FRAMES,\n  delayInFrames = 0,\n  spring: springProp,\n  exit = false,\n  exitInFrames,\n  exitAtInFrames,\n  exitTravel = DEFAULT_EXIT_TRAVEL,\n  exitDirection = \"reverse\",\n}: EnterExitOptions): MotionState {\n  const frame = useCurrentFrame();\n  const { fps, durationInFrames: windowFrames } = useVideoConfig();\n\n  const duration = Math.max(1, Math.round(durationInFrames));\n  const delay = Math.max(0, Math.round(delayInFrames));\n\n  const enterTime = interpolate(\n    frame,\n    [delay, delay + duration],\n    [0, 1],\n    clamp,\n  );\n\n  const enterMotion = springProp\n    ? spring({\n        frame,\n        fps,\n        config: resolveSpringConfig(springProp),\n        delay,\n        durationInFrames: duration,\n      })\n    : interpolate(enterTime, [0, 1], [0, 1], {\n        easing: EASING_ARRIVE,\n        ...clamp,\n      });\n\n  const enterOpacity = interpolate(\n    enterTime,\n    [0, ENTER_OPACITY_LEAD],\n    [0, 1],\n    { easing: EASING_ENTER, ...clamp },\n  );\n\n  const exitFrames =\n    exitInFrames === undefined\n      ? defaultExitFrames(duration)\n      : Math.max(MIN_EXIT_FRAMES, Math.round(exitInFrames));\n  const hasWindow = Number.isFinite(windowFrames);\n  const requested =\n    exitAtInFrames ?? (hasWindow ? windowFrames - exitFrames : null);\n  const wantsExit =\n    exit || exitInFrames !== undefined || exitAtInFrames !== undefined;\n\n  if (!wantsExit || requested === null) {\n    return {\n      motion: enterMotion,\n      displace: 1 - enterMotion,\n      opacity: enterOpacity,\n      sign: 1,\n      exiting: false,\n      exitProgress: 0,\n      enterTime,\n    };\n  }\n\n  /* An exit that starts before the entrance has landed reads as a stutter, so\n   * it is pushed behind the entrance rather than overlapping it. */\n  const exitStart = Math.max(requested, delay + duration);\n  const exitTime = interpolate(\n    frame,\n    [exitStart, exitStart + exitFrames],\n    [0, 1],\n    clamp,\n  );\n  const exitProgress = interpolate(exitTime, [0, 1], [0, 1], {\n    easing: EASING_EXIT,\n    ...clamp,\n  });\n  const exitOpacity = interpolate(\n    exitTime,\n    [0, EXIT_OPACITY_LEAD],\n    [1, 0],\n    { easing: EASING_EXIT, ...clamp },\n  );\n  const exiting = frame >= exitStart;\n\n  return {\n    motion: exiting ? 1 - exitProgress : enterMotion,\n    displace: exiting ? exitProgress * exitTravel : 1 - enterMotion,\n    opacity: enterOpacity * exitOpacity,\n    sign: exiting && exitDirection === \"continue\" ? -1 : 1,\n    exiting,\n    exitProgress,\n    enterTime,\n  };\n}\n\nexport type TransformOrigin =\n  | \"center\"\n  | \"top\"\n  | \"bottom\"\n  | \"left\"\n  | \"right\"\n  | \"top left\"\n  | \"top right\"\n  | \"bottom left\"\n  | \"bottom right\";\n\n/** Named origins map straight to CSS, `center` included, for prop ergonomics. */\nexport function resolveOrigin(origin: TransformOrigin): string {\n  return origin === \"center\" ? \"center center\" : origin;\n}\n"
    }
  ]
}