{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-motion",
  "title": "Eased motion",
  "description": "The named springs (snappy, smooth, bouncy), tweens, a hydration-safe useSafeReducedMotion and press classes every Eased component shares, matching the springs in the Eased stylesheet, plus small DOM and platform helpers (isEditable, useScrollFades, useIsMac) and a true-minus number formatter.",
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/lib/motion.ts",
      "content": "/**\n * Eased motion tokens for Motion (motion/react), mirroring `eased.css`.\n * The CSS springs (`--eased-spring-*`) are these same springs sampled as linear(),\n * so a CSS transition and a Motion animation with the same name feel identical.\n *\n *   snappy  presses, toggles, thumbs, indicators, text swaps, rolling digits, focus ring\n *   smooth  layout, FLIP, panels, toast and tooltip position (no overshoot)\n *   bouncy  shape morphs and playful reveals only; never exits\n */\n\nimport { useSyncExternalStore } from \"react\";\n\nexport const snappy = { type: \"spring\", stiffness: 520, damping: 38 } as const;\nexport const smooth = { type: \"spring\", visualDuration: 0.4, bounce: 0 } as const;\nexport const bouncy = { type: \"spring\", stiffness: 400, damping: 26 } as const;\n\n/** Cubic-bezier curves, same as `--eased-ease-enter`, `--eased-ease-standard`, `--eased-ease-exit`, `--eased-ease-in-out`. */\nexport const ease = {\n  enter: [0.16, 1, 0.3, 1],\n  standard: [0.22, 1, 0.36, 1],\n  exit: [0.7, 0, 0.84, 0],\n  inOut: [0.65, 0, 0.35, 1],\n} as const;\n\n/** Seconds, same as `--eased-duration-*`. */\nexport const duration = {\n  instant: 0.1,\n  fast: 0.15,\n  standard: 0.2,\n  slow: 0.3,\n  sheet: 0.5,\n  /** Before an image fallback (Avatar initials) shows, so a fast load never flashes it. */\n  fallback: 0.4,\n} as const;\n\n/** Seconds between staggered children (rule 83); keep the total ≤ 0.4s. */\nexport const stagger = { char: 0.016, item: 0.035, word: 0.04, line: 0.08 } as const;\n\n/** Blur radii in motion: `soft` for text and icon swaps; `text` is the most anything may blur. */\nexport const blur = { subtle: \"2px\", soft: \"4px\", text: \"8px\" } as const;\n\n/** Things that appear: opacity and blur alongside a spring. */\nexport const enter = { duration: duration.standard, ease: ease.enter } as const;\n/** Opacity and colour while something stays on screen. */\nexport const fade = { duration: duration.fast, ease: ease.standard } as const;\n/** Things that leave: ≤ 0.75 × `enter`, ease-in, never overshoots. */\nexport const exit = { duration: duration.fast, ease: ease.exit } as const;\n/** Small things that leave: buttons, chips, the outgoing half of a swap. */\nexport const exitFast = { duration: duration.instant, ease: ease.exit } as const;\n/** The reduced-motion branch: opacity only, at `--eased-duration-instant`. */\nexport const reducedFade = { duration: duration.instant } as const;\nexport const instant = { duration: 0 } as const;\n\n/** Text and icon swaps (rule 82): out up 3px, in from 4px below, blur tweened apart from the spring. */\nexport const swapIn = { opacity: 0, y: 4, filter: `blur(${blur.soft})` } as const;\nexport const swapShown = { opacity: 1, y: 0, filter: \"blur(0px)\" } as const;\nexport const swapOut = { opacity: 0, y: -3, filter: `blur(${blur.soft})` } as const;\nexport const swap = { ...snappy, opacity: fade, filter: fade } as const;\n\nconst REDUCE = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const mq = window.matchMedia(REDUCE);\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n\n/**\n * `prefers-reduced-motion`, read after hydration (rule 86): false on the server and during\n * hydration, the real value from then on, and live when the setting changes. Use this, not\n * Motion's `useReducedMotion`, which reads the media query during the first client render\n * and so mismatches the server HTML for reduced-motion users.\n */\nexport function useSafeReducedMotion() {\n  return useSyncExternalStore(subscribeReduced, () => window.matchMedia(REDUCE).matches, () => false);\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/lib/motion.ts"
    },
    {
      "path": "src/registry/eased/lib/press.ts",
      "content": "/**\n * Press feedback as one class string per control size (rule 63). Colour moves at 150ms on\n * the standard curve; scale presses down in `--eased-duration-instant` and releases on the snappy\n * spring. Keyboard focus draws Eased's ring (`focus-ring`). Append your own colour classes;\n * do not add another `transition-*` class.\n *\n *   press      text buttons (`--eased-press-scale`, 0.97)\n *   pressIcon  icon buttons ≤ 48px (`--eased-press-scale-icon`, 0.96)\n *   pressWide  bars and cards > 220px (`--eased-press-scale-wide`, 0.985)\n */\nconst base =\n  \"[transition:color_150ms_var(--eased-ease-standard),background-color_150ms_var(--eased-ease-standard),border-color_150ms_var(--eased-ease-standard),opacity_150ms_var(--eased-ease-standard),scale_var(--eased-duration-spring-snappy)_var(--eased-spring-snappy),box-shadow_var(--eased-duration-focus)_var(--eased-ease-standard)] active:duration-(--eased-duration-instant) active:ease-standard reduced:active:scale-none focus-ring\";\n\nexport const press = `${base} active:scale-(--eased-press-scale)`;\nexport const pressIcon = `${base} active:scale-(--eased-press-scale-icon)`;\nexport const pressWide = `${base} active:scale-(--eased-press-scale-wide)`;\n",
      "type": "registry:lib",
      "target": "@components/eased/lib/press.ts"
    },
    {
      "path": "src/registry/eased/lib/dom.ts",
      "content": "import { useEffect, type RefObject } from \"react\";\n\n/** True when a key event lands in a text field, so global shortcuts leave typing alone. */\nexport function isEditable(t: EventTarget | null) {\n  return t instanceof HTMLElement && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));\n}\n\n/**\n * 20px scroll feathers, each only while there is content past that edge (R94). Sets\n * `--fade-top`/`--fade-bottom` (axis \"y\") or `--fade-start`/`--fade-end` (axis \"x\", read by\n * the `fade-x` utility) on the scroller; pair them with a mask-image.\n */\nexport function useScrollFades(ref: RefObject<HTMLElement | null>, axis: \"x\" | \"y\" = \"y\") {\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const [start, end] = axis === \"x\" ? [\"--fade-start\", \"--fade-end\"] : [\"--fade-top\", \"--fade-bottom\"];\n    const update = () => {\n      const [pos, size, view] = axis === \"x\" ? [el.scrollLeft, el.scrollWidth, el.clientWidth] : [el.scrollTop, el.scrollHeight, el.clientHeight];\n      el.style.setProperty(start, pos > 1 ? \"20px\" : \"0px\");\n      el.style.setProperty(end, size - view - pos > 1 ? \"20px\" : \"0px\");\n    };\n    update();\n    el.addEventListener(\"scroll\", update, { passive: true });\n    const ro = new ResizeObserver(update);\n    ro.observe(el);\n    if (el.firstElementChild) ro.observe(el.firstElementChild);\n    return () => {\n      el.removeEventListener(\"scroll\", update);\n      ro.disconnect();\n    };\n  }, [ref, axis]);\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/lib/dom.ts"
    },
    {
      "path": "src/registry/eased/lib/platform.ts",
      "content": "import { useSyncExternalStore } from \"react\";\n\nconst subscribe = () => () => {};\nconst isMac = () => /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);\n\n/**\n * True on Apple platforms, read after hydration: the server (and the hydrating render) says\n * Mac, so shortcut hints render ⌘ first and correct to Ctrl on other systems.\n */\nexport function useIsMac() {\n  return useSyncExternalStore(subscribe, isMac, () => true);\n}\n\n/** The printed modifier for shortcuts: \"⌘\" on Apple platforms, \"Ctrl\" elsewhere. */\nexport function modKey(mac: boolean) {\n  return mac ? \"⌘\" : \"Ctrl\";\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/lib/platform.ts"
    },
    {
      "path": "src/registry/eased/lib/format.ts",
      "content": "/**\n * Number formatting shared by the finance and crypto blocks. Intl writes negatives with a\n * hyphen-minus (U+002D), which is narrower than \"+\" and breaks a tabular column; Eased uses the\n * true minus sign (U+2212) everywhere.\n */\n\n/** Swap Intl's hyphen-minus for the true minus sign. */\nexport const trueMinus = (text: string) => text.replace(/-/g, \"−\");\n\n/**\n * An Intl formatter that writes negatives with the true minus sign. Pass it to NumberRoll as\n * `formatter`, or call it directly: `const pct = minusFormatter(locale, { style: \"percent\" })`.\n */\nexport function minusFormatter(locale: Intl.LocalesArgument, options: Intl.NumberFormatOptions): (value: number) => string {\n  const format = new Intl.NumberFormat(locale, options);\n  return (value) => trueMinus(format.format(value));\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/lib/format.ts"
    }
  ],
  "type": "registry:lib"
}