{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "motion",
  "name": "Motion",
  "category": "Layout & utilities",
  "description": "A small motion system: three durations, three curves, the noodle’s own entrance for anything you add, staggered lists, press and lift feedback, drawn lines, counted numbers and cross-faded changes. All of it stops under reduced motion.",
  "tags": [
    "motion",
    "animation",
    "transition",
    "easing",
    "stagger",
    "entrance",
    "count up",
    "view transition",
    "micro interaction",
    "hover"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "animation",
      "entrance animation",
      "staggered list",
      "hover lift",
      "count up number",
      "page transition",
      "micro interactions"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": "Do not fade sections in on scroll and do not animate everything. Motion explains a change: something arrived, moved or was pressed."
  },
  "url": "https://mian.ro/components/motion",
  "markdownUrl": "https://mian.ro/ai/components/motion.md",
  "classes": [
    {
      "name": "--mn-dur-fast · --mn-dur · --mn-dur-slow",
      "description": "120, 200 and 360 ms: feedback, state change, layout change."
    },
    {
      "name": "--mn-ease-out · --mn-ease-in-out · --mn-ease-spring",
      "description": "Arriving, moving across the screen, and a soft overshoot for playful noodles."
    },
    {
      "name": "mn-enter",
      "description": "Plays the current noodle’s entrance once, when the element is added."
    },
    {
      "name": "mn-stagger",
      "description": "On a container: its first eight children enter 45 ms apart (--mn-stagger-step)."
    },
    {
      "name": "mn-press",
      "description": "Shrinks to 97% while pressed."
    },
    {
      "name": "mn-lift",
      "description": "Rises 2px with a shadow on hover, for whole-card links."
    },
    {
      "name": "mn-draw",
      "description": "On an SVG path with pathLength=\"1\": draws the line in."
    },
    {
      "name": "data-mn-count",
      "description": "Counts up to the element’s own number once, when first seen."
    },
    {
      "name": "Mian.transition(fn)",
      "description": "Runs a DOM change inside a view transition, so it cross-fades. Mian.setNoodle and Mian.setMode use it."
    }
  ],
  "accessibility": [
    "Everything here is disabled by prefers-reduced-motion: reduce; nothing depends on an animation finishing.",
    "Animate to explain a change (something arrived, moved, was pressed), not to decorate. Do not fade sections in as the reader scrolls.",
    "Keep motion under 400 ms and never loop it near text.",
    "Counted numbers keep their final value in the DOM, so assistive technology never reads a partial count."
  ],
  "examples": [
    {
      "title": "Entrance and stagger",
      "description": "Press the button: the list is re-inserted and enters in this noodle’s own style.",
      "html": "<div class=\"mn-stack\" style=\"width:min(100%,22rem)\">\n  <ul class=\"mn-list mn-stagger\" id=\"motion-list\"><li>Invoice sent</li><li>Reminder scheduled</li><li>Payment received</li><li>Receipt emailed</li></ul>\n  <button class=\"mn-btn mn-btn-outline mn-press\" type=\"button\" onclick=\"const l=document.getElementById('motion-list');const c=l.cloneNode(true);l.replaceWith(c)\">Play again</button>\n</div>"
    },
    {
      "title": "Press, lift and count",
      "description": null,
      "html": "<div class=\"mn-row\" style=\"--mn-gap:1rem\">\n  <a class=\"mn-card mn-lift\" href=\"#\" style=\"display:block;padding:1rem 1.25rem;text-decoration:none;color:inherit\"><span class=\"mn-muted\" style=\"font-size:.8125rem\">Orders today</span><br><strong style=\"font-size:1.5rem\" data-mn-count>1,284</strong></a>\n  <button class=\"mn-btn mn-press\" type=\"button\">Press me</button>\n</div>"
    }
  ],
  "css": ".mn-enter { animation: var(--mn-enter) var(--mn-enter-duration) var(--mn-ease-enter) both; }\n.mn-stagger > * { animation: var(--mn-enter) var(--mn-enter-duration) var(--mn-ease-enter) both; animation-delay: calc(var(--mn-stagger-step, 45ms) * var(--i, 8)); }\n.mn-stagger > :nth-child(1) { --i: 0; } .mn-stagger > :nth-child(2) { --i: 1; } .mn-stagger > :nth-child(3) { --i: 2; } .mn-stagger > :nth-child(4) { --i: 3; }\n.mn-stagger > :nth-child(5) { --i: 4; } .mn-stagger > :nth-child(6) { --i: 5; } .mn-stagger > :nth-child(7) { --i: 6; } .mn-stagger > :nth-child(8) { --i: 7; }\n.mn-press { transition: scale var(--mn-dur-fast) var(--mn-ease-out); }\n.mn-press:active { scale: .97; }\n@media (hover: hover) {\n  .mn-lift { transition: translate var(--mn-dur) var(--mn-ease-out), box-shadow var(--mn-dur) var(--mn-ease-out); }\n  .mn-lift:hover { translate: 0 -2px; box-shadow: var(--mn-shadow-md); }\n}\n.mn-draw { stroke-dasharray: 1; stroke-dashoffset: 0; animation: mn-draw var(--mn-dur-draw, 900ms) var(--mn-ease-out) both; }\n@keyframes mn-draw { from { stroke-dashoffset: 1; } }\n@keyframes mn-vt-out { to { opacity: 0; } }\n@keyframes mn-vt-in { from { opacity: 0; } }\nhtml.mn-vt::view-transition-old(root) { animation: mn-vt-out 140ms var(--mn-ease-out) both; }\nhtml.mn-vt::view-transition-new(root) { animation: mn-vt-in 240ms var(--mn-ease-out) both; }\n@media (prefers-reduced-motion: reduce) {\n  .mn-enter, .mn-stagger > *, .mn-draw { animation: none; }\n  .mn-press, .mn-lift { transition: none; }\n  .mn-press:active { scale: none; } .mn-lift:hover { translate: none; }\n}\n}\n@layer mian.components {",
  "js": "/* Mian.transition(update): runs a DOM update inside a view transition, so the\n   change cross-fades instead of snapping. Falls back to a plain call where the\n   API is missing or the reader prefers reduced motion.\n   <span data-mn-count>€45,231</span> counts up to its own text once, the first\n   time it is seen. The final text stays in the DOM for assistive tech. */\nconst calm = () => matchMedia('(prefers-reduced-motion: reduce)').matches;\nMian.transition = (update) => {\n  if (!document.startViewTransition || calm() || document.visibilityState !== 'visible') { update(); return null; }\n  const root = document.documentElement;\n  root.classList.add('mn-vt');\n  let vt;\n  try { vt = document.startViewTransition(update); } catch (_) { root.classList.remove('mn-vt'); update(); return null; }\n  vt.finished.finally(() => root.classList.remove('mn-vt'));\n  return vt;\n};\nfunction countUp(el) {\n  const text = el.textContent, m = text.match(/-?\\d[\\d.,\\s]*\\d|\\d/);\n  if (!m || calm()) return;\n  const raw = m[0], decimal = /[.,]\\d{1,2}$/.exec(raw);\n  const sep = decimal ? decimal[0][0] : '', digits = decimal ? decimal[0].length - 1 : 0;\n  const group = (raw.replace(decimal ? decimal[0] : '', '').match(/[.,\\s]/) || [''])[0];\n  const target = Number(raw.replace(decimal ? decimal[0] : '', '').replace(/[.,\\s]/g, '') + (decimal ? '.' + decimal[0].slice(1) : ''));\n  if (!isFinite(target)) return;\n  const fmtN = (v) => { const [a, b] = v.toFixed(digits).split('.'); return a.replace(/\\B(?=(\\d{3})+(?!\\d))/g, group) + (digits ? sep + b : ''); };\n  const pre = text.slice(0, m.index), post = text.slice(m.index + raw.length);\n  el.setAttribute('aria-label', text.trim());\n  const t0 = performance.now(), dur = 700;\n  const step = (t) => {\n    const k = Math.min(1, (t - t0) / dur), e = 1 - Math.pow(1 - k, 4);\n    el.textContent = pre + fmtN(target * e) + post;\n    if (k < 1) requestAnimationFrame(step); else { el.textContent = text; el.removeAttribute('aria-label'); }\n  };\n  requestAnimationFrame(step);\n}\nregister('motion', (root) => each(root, '[data-mn-count]', 'Count', (el) => {\n  if (!('IntersectionObserver' in window)) return;\n  const io = new IntersectionObserver((entries) => { if (entries.some((x) => x.isIntersecting)) { io.disconnect(); countUp(el); } }, { threshold: .6 });\n  io.observe(el);\n}));",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/motion.json"
  }
}
