{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "motion-core",
  "title": "Motion Core",
  "description": "BoldKit Motion core — framework-agnostic, SSR-safe primitives (observeReveal, staggerChildren, triggerAnimation, startViewTransition, prefersReducedMotion) shared by every motion adapter.",
  "files": [
    {
      "path": "registry/default/lib/motion-core.ts",
      "content": "/**\n * BoldKit Motion Core — framework-agnostic primitives.\n *\n * Consumed by every L3 adapter (React, Vue, future Svelte/Solid/Qwik).\n * Adding a new framework = wrap these functions. No CSS or token changes needed.\n *\n * All functions are SSR-safe: they no-op on the server.\n */\n\nconst isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\n\n// ──────────────────────────────────────────────────────────────────\n// Reduced motion detection (cached; updates on media-query change)\n// ──────────────────────────────────────────────────────────────────\n\nlet _reducedMotion = false;\nlet _reducedMotionMql: MediaQueryList | null = null;\n\nif (isBrowser) {\n  _reducedMotionMql = window.matchMedia('(prefers-reduced-motion: reduce)');\n  _reducedMotion = _reducedMotionMql.matches;\n  _reducedMotionMql.addEventListener('change', (e) => {\n    _reducedMotion = e.matches;\n  });\n}\n\nexport function prefersReducedMotion(): boolean {\n  return _reducedMotion;\n}\n\n// ──────────────────────────────────────────────────────────────────\n// observeReveal — IntersectionObserver-driven scroll reveal\n// ──────────────────────────────────────────────────────────────────\n\nexport type RevealOptions = {\n  /** Margin around the root. Negative bottom fires reveal slightly before viewport entry. */\n  rootMargin?: string;\n  /** Intersection ratio threshold. 0 = any pixel visible. */\n  threshold?: number;\n  /** If true, unobserves after first reveal. Default true. */\n  once?: boolean;\n  /** Delay before applying the reveal-in class, in ms. */\n  delay?: number;\n};\n\nconst DEFAULT_REVEAL_OPTS: Required<RevealOptions> = {\n  rootMargin: '0px 0px -10% 0px',\n  threshold: 0,\n  once: true,\n  delay: 0,\n};\n\n/**\n * Observes `el` and toggles `.bk-reveal-in` when it enters the viewport.\n * The element must have the `.bk-reveal` class (and optionally a direction\n * modifier like `.bk-reveal-up`) already applied for the CSS animation to fire.\n *\n * Returns a cleanup function — call on unmount.\n */\nexport function observeReveal(el: Element, opts?: RevealOptions): () => void {\n  if (!isBrowser) return () => {};\n\n  // Reduced motion: show immediately, no animation. The L1 CSS guard\n  // disables the animation declaration; we just remove the hidden state.\n  if (_reducedMotion) {\n    el.classList.remove('bk-reveal');\n    el.classList.add('bk-reveal-in');\n    return () => {};\n  }\n\n  const merged = { ...DEFAULT_REVEAL_OPTS, ...opts };\n\n  const observer = new IntersectionObserver(\n    (entries, obs) => {\n      for (const entry of entries) {\n        if (!entry.isIntersecting) continue;\n        const target = entry.target;\n        if (merged.delay > 0) {\n          window.setTimeout(() => target.classList.add('bk-reveal-in'), merged.delay);\n        } else {\n          target.classList.add('bk-reveal-in');\n        }\n        if (merged.once) obs.unobserve(target);\n      }\n    },\n    { rootMargin: merged.rootMargin, threshold: merged.threshold }\n  );\n\n  observer.observe(el);\n  return () => observer.disconnect();\n}\n\n// ──────────────────────────────────────────────────────────────────\n// staggerChildren — apply incrementing animation-delay to children\n// ──────────────────────────────────────────────────────────────────\n\nexport type StaggerOptions = {\n  /** Delay between siblings, in ms. Default 75. */\n  delay?: number;\n  /** Initial delay before the first child, in ms. */\n  initialDelay?: number;\n  /** CSS selector to filter children. Default '*' (all). */\n  selector?: string;\n};\n\n/**\n * Sets `animation-delay` on each matched child of `root` so they animate\n * in sequence. Pairs with any of the bk-* entrance animations.\n *\n * Returns a cleanup function that clears the delays.\n */\nexport function staggerChildren(root: Element, opts?: StaggerOptions): () => void {\n  if (!isBrowser) return () => {};\n  if (_reducedMotion) return () => {};\n\n  const { delay = 75, initialDelay = 0, selector = '*' } = opts ?? {};\n  const children = Array.from(root.querySelectorAll(selector)).filter(\n    (c) => c.parentElement === root\n  ) as HTMLElement[];\n\n  for (let i = 0; i < children.length; i++) {\n    children[i].style.animationDelay = `${initialDelay + i * delay}ms`;\n  }\n\n  return () => {\n    for (const child of children) child.style.animationDelay = '';\n  };\n}\n\n// ──────────────────────────────────────────────────────────────────\n// triggerAnimation — imperative one-shot animation\n// ──────────────────────────────────────────────────────────────────\n\n/**\n * Adds `className` to `el`, awaits the animation to complete, then removes it.\n * Used by `useShake()` and similar imperative APIs.\n *\n * Resolves immediately under reduced motion.\n */\nexport function triggerAnimation(el: Element, className: string): Promise<void> {\n  if (!isBrowser || _reducedMotion) return Promise.resolve();\n\n  return new Promise<void>((resolve) => {\n    const onEnd = () => {\n      el.removeEventListener('animationend', onEnd);\n      el.classList.remove(className);\n      resolve();\n    };\n    el.addEventListener('animationend', onEnd, { once: true });\n    // Force reflow so the class addition triggers a fresh animation\n    // even if the same class was applied moments ago.\n    el.classList.remove(className);\n    void (el as HTMLElement).offsetWidth;\n    el.classList.add(className);\n  });\n}\n\n// ──────────────────────────────────────────────────────────────────\n// startViewTransition — View Transitions API wrapper\n// ──────────────────────────────────────────────────────────────────\n\ntype ViewTransitionLike = {\n  ready: Promise<void>;\n  finished: Promise<void>;\n  updateCallbackDone: Promise<void>;\n};\n\n/**\n * Wraps `document.startViewTransition` with a no-op fallback for browsers\n * that don't support it (Firefox as of writing). Returns null when unsupported,\n * so callers can detect and skip awaiting.\n */\nexport function startViewTransition(callback: () => void | Promise<void>): ViewTransitionLike | null {\n  if (!isBrowser || _reducedMotion) {\n    void callback();\n    return null;\n  }\n  const doc = document as Document & {\n    startViewTransition?: (cb: () => void | Promise<void>) => ViewTransitionLike;\n  };\n  if (typeof doc.startViewTransition !== 'function') {\n    void callback();\n    return null;\n  }\n  return doc.startViewTransition(callback);\n}\n",
      "type": "registry:lib",
      "target": "lib/motion-core.ts"
    }
  ],
  "type": "registry:lib"
}