{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"rollback-orbit","type":"registry:ui","title":"Rollback Orbit","description":"A weighted ball rolls to its target and back, rocking the rail it runs on and settling the shadow underneath. Press to roll.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/rollback-orbit.tsx","target":"components/ui/rollback-orbit.tsx","content":"'use client';\n\nimport { useState } from 'react';\n\nimport './rollback-orbit.css';\n\nexport type RollbackOrbitProps = {\n  /** Shown under the rail before the roll. */\n  label?: string;\n  /** Replaces it while the ball is travelling. */\n  activeLabel?: string;\n  /**\n   * The 298x240 catalogue card. The same rail, ball and keyframes at exactly their\n   * authored size — a 298px frame is wide enough for a 280px rail, so nothing is\n   * scaled — with the corner wordmark dropped and the label moved onto the bottom\n   * edge, where a 240px box can actually show it. Presentation only, and all of it\n   * in `rollback-orbit.css`.\n   */\n  compact?: boolean;\n};\n\nexport function RollbackOrbit({\n  label = 'Press to roll',\n  activeLabel = 'In motion',\n  compact = false,\n}: RollbackOrbitProps = {}) {\n  const [rolling, setRolling] = useState(false);\n  const run = () => {\n    if (rolling) return;\n    setRolling(true);\n    window.setTimeout(() => setRolling(false), 2100);\n  };\n  return (\n    <div className=\"rollback-orbit-stage\" data-compact={compact ? 'true' : undefined}>\n      {/* In a card the frame is aria-hidden, and a focusable node inside one is a trap\n          with no name — the card's own title link is the way to the item. The press\n          still works in both: only the tab stop goes. */}\n      <button\n        type=\"button\"\n        className={`rollback-orbit ${rolling ? 'running' : ''}`}\n        onClick={run}\n        tabIndex={compact ? -1 : undefined}\n        aria-label=\"Roll the ball to its target and back\"\n      >\n        <span className=\"roll-rail\">\n          <i className=\"roll-ball\">\n            <b />\n          </i>\n          <i className=\"roll-target\" />\n        </span>\n        <em>{rolling ? activeLabel : label}</em>\n      </button>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/rollback-orbit.css","target":"components/ui/rollback-orbit.css","content":"@keyframes roll-across {\n  0% {\n    transform: translateX(0) rotate(0);\n  }\n  44% {\n    transform: translateX(202px) rotate(600deg);\n  }\n  63% {\n    transform: translateX(210px) rotate(670deg);\n  }\n  100% {\n    transform: translateX(0) rotate(1080deg);\n  }\n}\n\n@keyframes roll-rail-rock {\n  45% {\n    transform: rotate(3deg);\n  }\n  72% {\n    transform: rotate(-5deg);\n  }\n}\n\n@keyframes roll-target-hit {\n  45% {\n    scale: 1;\n  }\n  54% {\n    scale: 0.72;\n  }\n  68% {\n    scale: 1.15;\n  }\n  100% {\n    scale: 1;\n  }\n}\n\n@keyframes roll-rail-shadow {\n  50% {\n    scale: 0.88;\n    filter: blur(19px);\n    opacity: 0.52;\n  }\n}\n\n/*\n * The surface the button is designed to sit on. Also the positioning context\n * for the rail's ground shadow, which is absolute with auto offsets.\n */\n.rollback-orbit-stage {\n  position: relative;\n  display: grid;\n  place-items: center;\n  width: 100%;\n  height: 100%;\n  min-height: 240px;\n  container-type: inline-size;\n  overflow: hidden;\n  background: #d8ff43;\n}\n\n.rollback-orbit {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  display: grid;\n  place-items: center;\n  border: 0;\n  background: radial-gradient(circle at 50% 43%, #eeff9a 0, #d8ff43 47%, #9dad2f 100%);\n}\n\n/* The rail's ground shadow, breathing on its own slower clock. */\n.rollback-orbit::before {\n  content: '';\n  position: absolute;\n  width: 330px;\n  height: 86px;\n  border-radius: 60px;\n  background: rgba(17, 17, 15, 0.1);\n  filter: blur(13px);\n  transform: translateY(32px) rotate(-3deg);\n  animation: roll-rail-shadow 2.8s ease-in-out infinite;\n}\n\n.rollback-orbit::after {\n  content: 'MOMENTUM / 01';\n  position: absolute;\n  right: 17px;\n  top: 15px;\n  font: 7px ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.14em;\n}\n\n.rollback-orbit .roll-rail {\n  position: relative;\n  width: 280px;\n  height: 72px;\n  border: 4px solid #111;\n  border-radius: 50px;\n  background: linear-gradient(#b7cf42, #edff9e);\n  box-shadow:\n    inset 0 12px 18px rgba(255, 255, 255, 0.5),\n    inset 0 -11px 18px rgba(17, 17, 15, 0.2),\n    0 19px 0 rgba(17, 17, 15, 0.1);\n  transform: rotate(-3deg);\n  transition: transform 0.25s;\n}\n\n.rollback-orbit .roll-ball {\n  position: absolute;\n  left: 9px;\n  top: 8px;\n  width: 50px;\n  height: 50px;\n  border-radius: 50%;\n  background: radial-gradient(\n    circle at 35% 28%,\n    #fff 0 4%,\n    #8d71ff 12%,\n    #342176 62%,\n    #17102d 100%\n  );\n  box-shadow:\n    inset -8px -10px 14px rgba(0, 0, 0, 0.45),\n    inset 7px 7px 10px rgba(255, 255, 255, 0.38),\n    5px 10px 14px rgba(0, 0, 0, 0.32);\n  transform-origin: center;\n}\n\n.rollback-orbit .roll-ball b {\n  position: absolute;\n  inset: 8px;\n  border: 1px dashed rgba(255, 255, 255, 0.35);\n  border-radius: 50%;\n}\n\n.rollback-orbit .roll-target {\n  position: absolute;\n  right: 14px;\n  top: 14px;\n  width: 40px;\n  height: 40px;\n  border: 2px dashed rgba(17, 17, 15, 0.35);\n  border-radius: 50%;\n}\n\n.rollback-orbit > em {\n  position: absolute;\n  margin-top: 132px;\n  font: 8px ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-style: normal;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n}\n\n.rollback-orbit.running .roll-rail {\n  animation: roll-rail-rock 2s ease-in-out;\n}\n\n.rollback-orbit.running .roll-ball {\n  animation: roll-across 2s cubic-bezier(0.5, 0.05, 0.16, 1);\n}\n\n.rollback-orbit.running .roll-target {\n  animation: roll-target-hit 2s ease;\n}\n\n@container (max-width: 520px) {\n  .rollback-orbit .roll-rail {\n    scale: 0.82;\n  }\n}\n\n/*\n * The card variant: the 298x240 catalogue frame, at that real size and never scaled.\n *\n * Nothing below shrinks the mechanism, because nothing has to. The rail is 280x72 and\n * the -3deg tilt puts its bounding box at 283x87, so it sits in a 298px frame with 7px\n * of lime at each side — the 4px border, the 50px ball, the 40px target and the 202px\n * of travel are the shipped pixels rather than a miniature of them. What was wrong at\n * this size was never the rail; it was the 132px margin under it and a 330px shadow in\n * a 298px box.\n *\n * No border-radius line: this stage never set one, and the card frame rounds and clips\n * already. No touch-action either — the only pointer target is a button answering to a\n * click, so this never becomes a drag surface that could claim the vertical gesture,\n * and a touch on the card goes on scrolling the page.\n */\n.rollback-orbit-stage[data-compact='true'] {\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * The bottom strip the label sits in — 12px of inset plus its line, near enough 30px —\n * taken out of the button's content box rather than off the rail. The rail is an\n * in-flow grid item and the ground shadow is an absolute child aligned in the same\n * content box, so this one value lifts both by 15px and the shadow stays under the\n * rail instead of drifting out from beneath it. The lift also puts the rail's centre on\n * the 43% highlight of the button's radial gradient — 103px of 240 against a rail\n * centred at 105 — which the 320px stage has 22px above the rail rather than on it.\n */\n.rollback-orbit-stage[data-compact='true'] .rollback-orbit {\n  padding-bottom: 30px;\n  /* The press surface is the whole frame now and only the label says so; nothing gives\n     a bare <button> a pointer cursor. */\n  cursor: pointer;\n}\n\n/*\n * 330px of ground shadow in a 298px frame is 16px of soft slab cut off flat against\n * each edge, and the blur is not wide enough to hide the cut. 252px keeps it 14px\n * inside the rail's own ends, and its outer edge dies about 10px short of the frame\n * once the 13px blur is spent; 74px keeps the lower half clear of the label. The 2.8s\n * breathing is deliberately left alone — it is the whole reason the card has motion on\n * load, before anyone has pressed anything.\n */\n.rollback-orbit-stage[data-compact='true'] .rollback-orbit::before {\n  width: 252px;\n  height: 74px;\n}\n\n/*\n * 'MOMENTUM / 01' is decoration at 7px in the section and a collision in a card: it is\n * pinned 17px in and 15px down from the top right corner, which is exactly where the\n * card frame hangs its own \"Open\" link. This box carries one line of type and the label\n * under the rail is the one that earns it.\n */\n.rollback-orbit-stage[data-compact='true'] .rollback-orbit::after {\n  display: none;\n}\n\n/* Undoes the narrow-stage miniature above. 298px is enough for the rail at 1:1, and a\n   fractional scale only softens a 4px border and two dashed rings. */\n.rollback-orbit-stage[data-compact='true'] .roll-rail {\n  scale: 1;\n}\n\n/*\n * The one line of type that stays, and the only one. Nothing about a lime slab says it\n * can be pressed, and this is the same <em> that turns over to 'In motion' while the\n * ball is out, so it carries the instruction and the state on one line.\n *\n * The 132px margin was measured for the 320px stage, where it clears the rail and\n * lands 23px above the floor; in a 240px box the same margin puts the label past the\n * bottom edge, where the stage's overflow: hidden eats it — the label is simply\n * missing today at card height. So it is anchored to the bottom edge instead. 10px\n * rather than the section's 8px because it is now the only text in the frame, and the\n * ink is pinned to the rail's own #111: the lime is a fixed colour in both themes, so\n * an inherited foreground would put white type on it in dark mode.\n */\n.rollback-orbit-stage[data-compact='true'] .rollback-orbit > em {\n  inset: auto 0 12px;\n  margin-top: 0;\n  font-size: 10px;\n  text-align: center;\n  color: #111;\n}\n\n/*\n * Two different kinds of motion live in this file and they get different answers.\n *\n * The ground shadow's `roll-rail-shadow` runs forever whether or not anyone is looking,\n * and the rail's hover transition is decoration; both go.\n *\n * The three `.running` animations stay. They are finite, and they only exist because the\n * visitor pressed the button — refusing to run them would leave the press with no answer\n * at all, which is a broken control rather than a calm one. `prefers-reduced-motion` asks\n * for less ambient movement, not for interactive parts to stop reporting what they did.\n */\n@media (prefers-reduced-motion: reduce) {\n  .rollback-orbit::before {\n    animation: none;\n  }\n\n  .rollback-orbit .roll-rail {\n    transition: none;\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["micro","springs","loaders"],"docs":"https://ui.artbloom.tech/artbloom/animations/rollback-orbit"}}