{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider",
  "title": "Motion engine (base)",
  "description": "The shared engine base every blaze-motion component needs — the single tune surface (the `feel` block in lib/motion.ts) plus the <MotionProvider>. Install once, retune everything from one place. Pulled in automatically by any component.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "lib/motion.ts",
      "content": "import type { TargetAndTransition, Transition, Variants } from \"motion/react\";\n\n/* ─────────────────────────────────────────────────────────────────────\n *  TUNE HERE — the whole engine's feel, in one place.\n *\n *  This is the only block you edit to retune blaze-motion for a project.\n *  Slower project? raise the durations. Faster? lower them. Every\n *  primitive derives from these values, so one edit re-tunes them all.\n *\n *  Per-instance overrides still win: <Fade delay={0.1} /> etc.\n * ───────────────────────────────────────────────────────────────────── */\nexport const feel = {\n  /** seconds — the three speeds the engine animates at */\n  duration: { fast: 0.2, base: 0.55, slow: 0.8 },\n  /** the tuned ease-out curve — a straight settle, no spring overshoot */\n  ease: [0.22, 0.61, 0.36, 1],\n  /** the softer ease used for page transitions */\n  easeSoft: [0.4, 0, 0.2, 1],\n  /** px a <StaggerItem> rises from (also Slide's `base` distance tier) */\n  rise: 28,\n  /** px a <Fade> drifts from — a FIXED, subtle reveal drift (Slide owns tunable travel) */\n  fadeShift: 16,\n  /** px a <Fade blur> focus-settles from — blur(Npx) → 0, opt-in (`filter` is GPU-costly) */\n  fadeBlur: 8,\n  /** how much of an element must be in view before it animates (0–1) */\n  inView: 0.3,\n  /** seconds between staggered children */\n  stagger: 0.08,\n  /** seconds before the first staggered child */\n  staggerDelay: 0.05,\n  /** seconds between staggered words (TextReveal, per-word) */\n  textStagger: 0.04,\n  /** seconds between staggered chars (TextReveal, per-char) */\n  textStaggerChar: 0.02,\n  /** px a TextReveal word/char rises from — shorter than a section rise */\n  textRise: 12,\n  /** spring pop — the ONE deliberate overshoot (SpringPop/RadialStagger); never titles */\n  spring: { stiffness: 300, damping: 15 },\n  /** scale a ScaleIn tween settles up FROM (tween, never spring — that's SpringPop) */\n  scaleFrom: 0.95,\n  /** Slide translate TIERS in px — `base` reuses `rise` (28); see `slideDistance` */\n  distance: { tight: 8, hero: 64 },\n  /** seconds between staggered lines (LineReveal / MaskTextReveal by line) */\n  lineStagger: 0.08,\n  /** px a LineReveal line rises from — between a word rise and a section rise */\n  lineRise: 14,\n  /** scale a GrowFromOrigin element grows up FROM, anchored to its transform-origin */\n  growScale: 0.85,\n  /** degrees a PerspectiveTiltIn element rotates on X from → 0 */\n  tiltAngle: 8,\n  /** px transform-perspective depth for PerspectiveTiltIn's rotateX */\n  perspective: 800,\n  /** percent a InsetFrameReveal clip-path insets uniformly from → 0% */\n  frameInset: 15,\n} as const;\n/* ───────────────────────────────────────────────────────────────────── */\n\n// Derived tokens — you rarely touch below this line.\n\nexport const durations = feel.duration;\n\nexport const ease = { out: feel.ease, soft: feel.easeSoft } as const;\n\nexport const revealTransition = {\n  duration: feel.duration.base,\n  ease: feel.ease,\n} as const satisfies Transition;\n\nexport const viewportOnce = { once: true, amount: feel.inView } as const;\n\nexport const fade = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n} as const;\n\n/** 5 directions a <Fade> drifts from as it fades in; `\"none\"` = pure opacity, no transform. */\nexport type FadeDirection = \"none\" | \"up\" | \"down\" | \"left\" | \"right\";\n\n// Unit sign per direction → the offset it drifts FROM (× feel.fadeShift). Mirrors slideOffset.\nconst fadeOffset: Record<FadeDirection, { x: number; y: number }> = {\n  none: { x: 0, y: 0 },\n  up: { x: 0, y: 1 },\n  down: { x: 0, y: -1 },\n  left: { x: 1, y: 0 },\n  right: { x: -1, y: 0 },\n};\n\n/**\n * <Fade> variants — fade + a FIXED subtle drift from `direction`, plus an optional\n * `blur`px → 0 focus-settle layered on top. The drift is fixed at `feel.fadeShift`\n * (tunable travel is Slide's job). No transition is baked in — the component folds\n * `delay` into `animate.transition` itself (the L004 variant-transition idiom).\n */\nexport const fadeVariants = (direction: FadeDirection = \"up\", blur = 0): Variants => {\n  const { x, y } = fadeOffset[direction];\n  const initial: TargetAndTransition = { opacity: 0, x: x * feel.fadeShift, y: y * feel.fadeShift };\n  const animate: TargetAndTransition = { opacity: 1, x: 0, y: 0 };\n  if (blur > 0) {\n    initial.filter = `blur(${blur}px)`;\n    animate.filter = \"blur(0px)\";\n  }\n  return { initial, animate };\n};\n\n/** scale-DOWN settle — for LARGE images only (never titles/text). */\nexport const cinematicScale = {\n  initial: { opacity: 0, scale: 1.1 },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\nexport const staggerContainer: Variants = {\n  initial: {},\n  animate: {\n    transition: { staggerChildren: feel.stagger, delayChildren: feel.staggerDelay },\n  },\n};\n\nexport const staggerItem: Variants = {\n  initial: { opacity: 0, y: feel.rise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n/** spring overshoot — the 0.8 → ~1.05 → 1 pop comes from the spring itself. */\nexport const springPopTransition = {\n  type: \"spring\",\n  stiffness: feel.spring.stiffness,\n  damping: feel.spring.damping,\n} as const satisfies Transition;\n\nexport const springPop = {\n  initial: { opacity: 0, scale: 0.8 },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\n/** per-word / per-char reveal — STRAIGHT rise, NO scale (the title rule). */\nexport const textRevealItem: Variants = {\n  initial: { opacity: 0, y: feel.textRise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n// Factory: TextReveal tunes its step per word vs char, plus an optional lead delay.\nexport const textRevealContainer = (\n  staggerChildren: number = feel.textStagger,\n  delayChildren = 0,\n): Variants => ({\n  initial: {},\n  animate: { transition: { staggerChildren, delayChildren } },\n});\n\n/* ── Entrance components (CARD-025 wave) — all straight `feel.ease` tweens ── */\n\n/** 8 travel directions a <Slide> enters along (diagonals move on both axes). */\nexport type SlideDirection =\n  | \"up\"\n  | \"down\"\n  | \"left\"\n  | \"right\"\n  | \"up-left\"\n  | \"up-right\"\n  | \"down-left\"\n  | \"down-right\";\n\n/** The three <Slide> distance tiers — `base` reuses `feel.rise`. */\nexport const slideDistance = {\n  tight: feel.distance.tight,\n  base: feel.rise,\n  hero: feel.distance.hero,\n} as const;\n\n// Unit sign per direction — travel dir → the offset it enters FROM (× distance).\nconst slideOffset: Record<SlideDirection, { x: number; y: number }> = {\n  up: { x: 0, y: 1 },\n  down: { x: 0, y: -1 },\n  left: { x: 1, y: 0 },\n  right: { x: -1, y: 0 },\n  \"up-left\": { x: 1, y: 1 },\n  \"up-right\": { x: -1, y: 1 },\n  \"down-left\": { x: 1, y: -1 },\n  \"down-right\": { x: -1, y: -1 },\n};\n\n/** fade + translate from `direction`; `distance` px defaults to the `base` tier. */\nexport const slideVariants = (\n  direction: SlideDirection,\n  distance: number = feel.rise,\n): Variants => {\n  const { x, y } = slideOffset[direction];\n  return {\n    initial: { opacity: 0, x: x * distance, y: y * distance },\n    animate: { opacity: 1, x: 0, y: 0, transition: revealTransition },\n  };\n};\n\n/** fade + scale-UP settle — a plain ease-out TWEEN (never a spring; that's SpringPop). */\nexport const scaleIn = {\n  initial: { opacity: 0, scale: feel.scaleFrom },\n  animate: { opacity: 1, scale: 1 },\n} as const;\n\n/** 4 edges a <ClipReveal> wipe reveals along; the box stays put. */\nexport type WipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\n// inset(T R B L) — one edge starts fully clipped (100%) and retracts to 0%.\n// All four strings share identical numeric slots so Motion diffs them cleanly.\nconst wipeInset: Record<WipeDirection, string> = {\n  up: \"inset(100% 0% 0% 0%)\",\n  down: \"inset(0% 0% 100% 0%)\",\n  left: \"inset(0% 0% 0% 100%)\",\n  right: \"inset(0% 100% 0% 0%)\",\n};\n\n/** clip-path edge wipe from `direction` → fully open `inset(0% 0% 0% 0%)`. */\nexport const wipeVariants = (direction: WipeDirection): Variants => ({\n  initial: { clipPath: wipeInset[direction] },\n  animate: { clipPath: \"inset(0% 0% 0% 0%)\", transition: revealTransition },\n});\n\n/** per-line rise + fade — one <LineReveal> line. Pair with `lineRevealContainer`. */\nexport const lineRevealItem: Variants = {\n  initial: { opacity: 0, y: feel.lineRise },\n  animate: { opacity: 1, y: 0, transition: revealTransition },\n};\n\n/** <LineReveal> container — reuses the text-stagger factory at the line step. */\nexport const lineRevealContainer = (delayChildren = 0): Variants =>\n  textRevealContainer(feel.lineStagger, delayChildren);\n\n/** transform-origin anchors a <GrowFromOrigin> grows from. */\nexport type Origin =\n  | \"top\"\n  | \"top-left\"\n  | \"top-right\"\n  | \"bottom\"\n  | \"bottom-left\"\n  | \"bottom-right\"\n  | \"left\"\n  | \"right\"\n  | \"center\";\n\n/** Origin token → CSS `transform-origin` string. */\nexport const originMap: Record<Origin, string> = {\n  top: \"top\",\n  \"top-left\": \"top left\",\n  \"top-right\": \"top right\",\n  bottom: \"bottom\",\n  \"bottom-left\": \"bottom left\",\n  \"bottom-right\": \"bottom right\",\n  left: \"left\",\n  right: \"right\",\n  center: \"center\",\n};\n\n/** fade + scale-UP from `origin`; transform-origin rides `style` (static, unanimated). */\nexport const growVariants = (origin: Origin = \"center\"): Variants => ({\n  initial: { opacity: 0, scale: feel.growScale, transformOrigin: originMap[origin] },\n  animate: {\n    opacity: 1,\n    scale: 1,\n    transformOrigin: originMap[origin],\n    transition: revealTransition,\n  },\n});\n\n/** fade + scaleX 0 → 1 from the left — a rule / underline draw. */\nexport const scaleXReveal = {\n  initial: { opacity: 0, scaleX: 0, transformOrigin: \"left\" },\n  animate: { opacity: 1, scaleX: 1, transformOrigin: \"left\" },\n} as const;\n\n/** fade + rise + rotateX settle over a perspective — a subtle 3D tilt-in. */\nexport const perspectiveTilt = {\n  initial: {\n    opacity: 0,\n    y: feel.rise,\n    rotateX: feel.tiltAngle,\n    transformPerspective: feel.perspective,\n  },\n  animate: {\n    opacity: 1,\n    y: 0,\n    rotateX: 0,\n    transformPerspective: feel.perspective,\n  },\n} as const;\n\n/** clip-path inset uniform `feel.frameInset`% → 0% — a frame that opens outward. */\nexport const insetFrameReveal = {\n  initial: {\n    clipPath: `inset(${feel.frameInset}% ${feel.frameInset}% ${feel.frameInset}% ${feel.frameInset}%)`,\n  },\n  animate: { clipPath: \"inset(0% 0% 0% 0%)\" },\n} as const;\n\n/** masked line/word — inner rise from below its overflow-hidden wrapper, NO opacity. */\nexport const maskTextItem: Variants = {\n  initial: { y: \"110%\" },\n  animate: { y: 0, transition: revealTransition },\n};\n\n/** fade + filter grayscale(1) → grayscale(0) — slow, images only (`filter` is costly). */\nexport const grayscaleReveal = {\n  initial: { opacity: 0, filter: \"grayscale(1)\" },\n  animate: { opacity: 1, filter: \"grayscale(0)\" },\n} as const;\n",
      "type": "registry:lib",
      "target": "@lib/motion.ts"
    },
    {
      "path": "components/motion/motion-provider.tsx",
      "content": "\"use client\";\n\nimport { domAnimation, LazyMotion, MotionConfig } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\n/**\n * Mount ONCE in your root layout, wrapping {children}.\n * LazyMotion `strict` means primitives must import `m` from \"motion/react-m\" —\n * a raw `motion.*` will throw. `reducedMotion=\"user\"` honors the OS setting.\n */\nexport function MotionProvider({ children }: { children: ReactNode }) {\n  return (\n    <LazyMotion features={domAnimation} strict>\n      <MotionConfig reducedMotion=\"user\">{children}</MotionConfig>\n    </LazyMotion>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/motion/motion-provider.tsx"
    }
  ],
  "docs": "Mount <MotionProvider> once in your root layout, wrapping {children}. Retune the whole engine from the `feel` block at the top of lib/motion.ts. LazyMotion is `strict`, so primitives import `m` from motion/react-m — a raw motion.* throws.",
  "type": "registry:block"
}