{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sula-motion",
  "type": "registry:ui",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "springs.ts",
      "target": "components/ui/springs.ts",
      "type": "registry:ui",
      "content": "/**\n * The sula springs. Every sula component animates on these so the family reads\n * as one fluid system: the presets share a natural-frequency register\n * (sqrt(stiffness / mass) near 4.8 rad/s), so nothing feels lighter or snappier\n * than its neighbours. Tune within the register, not away from it.\n */\n\n/** Underdamped on purpose: the bar lands with one soft dip and a faint second bob. */\nexport const barSpring = {\n  type: \"spring\",\n  stiffness: 68,\n  damping: 19,\n  mass: 2.9,\n} as const;\n/** Slower and heavier, so a side pill emerges sticky rather than snapping out. */\nexport const sideSpring = {\n  type: \"spring\",\n  stiffness: 70,\n  damping: 16,\n  mass: 2.1,\n  restDelta: 0.00075,\n  restSpeed: 0.0015,\n} as const;\n/** The whole row reshapes as one mass during a view switch. */\nexport const switchSpring = {\n  type: \"spring\",\n  stiffness: 115,\n  damping: 22,\n  mass: 1.45,\n} as const;\n/** The nav's body swelling open into the menu panel: heavy, underdamped, so the\n * bottom edge dips past its line and comes back. Same register, more mass: a\n * panel is more material than a pill. */\nexport const menuSwell = {\n  type: \"spring\",\n  stiffness: 90,\n  damping: 27,\n  mass: 3.9,\n} as const;\n/** Pulling the panel back in. Critically damped: material recoiling into a body\n * it never left does not bounce. */\nexport const menuRetract = {\n  type: \"spring\",\n  stiffness: 120,\n  damping: 49,\n  mass: 5.2,\n} as const;\n/** The leftover top tether pulls upward after the bar has landed. */\nexport const dripRetract = {\n  duration: 1.05,\n  ease: [0.3, 1.12, 0.36, 1],\n} as const;\n/** Labels come up once, promptly, as the bar lands. */\nexport const textFade = { duration: 0.22, ease: [0.22, 1, 0.36, 1] } as const;\n\nexport type SulaMotionSpec =\n  | typeof barSpring\n  | typeof sideSpring\n  | typeof menuSwell\n  | typeof menuRetract\n  | typeof switchSpring\n  | typeof dripRetract\n  | typeof textFade;\n"
    },
    {
      "path": "curves.ts",
      "target": "components/ui/curves.ts",
      "type": "registry:ui",
      "content": "export const clamp01 = (t: number): number => Math.min(1, Math.max(0, t));\n\nexport const mix = (a: number, b: number, t: number): number => a + (b - a) * t;\n\n/** Hermite ramp between edges; 0 below `a`, 1 above `b`, eased in between. */\nexport const smoothstep = (a: number, b: number, x: number): number => {\n  if (a === b) return x < a ? 0 : 1;\n  const t = clamp01((x - a) / (b - a));\n  return t * t * (3 - 2 * t);\n};\n\n/** Quintic smootherstep: flat start, flat end, so a row glides as one mass. */\nexport const smoother = (t: number): number => {\n  const x = clamp01(t);\n  return x * x * x * (x * (x * 6 - 15) + 10);\n};\n\n/** Decelerating ease-out: sheds most of the change in the first frames. */\nexport const easeOutCubic = (t: number): number => {\n  const inv = 1 - clamp01(t);\n  return 1 - inv * inv * inv;\n};\n\n/**\n * The \"bounce without a stop\" mapping, the heart of every sula settle. A part\n * must cross its rest line still moving, or a spring's overshoot reads as a dead\n * stop followed by a separate bob. `start` delays the launch; the ease reaches 1\n * at t=1 with slope 1, and raw t past 1 flows straight on to carry the overshoot\n * as one motion. Feed it the raw spring value, not a clamped one.\n */\nexport const c1Settle = (t: number, start: number): number => {\n  const f = Math.max(0, (t - start) / (1 - start));\n  return f < 1 ? f * f * (2 - f) : f;\n};\n"
    },
    {
      "path": "energy.ts",
      "target": "components/ui/energy.ts",
      "type": "registry:ui",
      "content": "export interface EnergyTracker {\n  readonly value: number;\n  bump(speed: number): number;\n  parked(): boolean;\n}\n\nexport interface EnergyOptions {\n  attack?: number;\n  decay?: number;\n  gain?: number;\n  parkBelow?: number;\n}\n\n/**\n * Tracks how lively the field is from measured per-frame speed, not from a\n * spring's finished promise: a spring resolves long after visible rest, so\n * releasing merge radius and wobble on it reads as a phantom width shift a second\n * late. Energy jumps to the current speed and decays each frame; the field parks\n * once it falls below the threshold.\n */\nexport function createEnergyTracker(\n  options: EnergyOptions = {},\n): EnergyTracker {\n  const attack = options.attack ?? 0.18;\n  const decay = options.decay ?? 0.9;\n  const gain = options.gain ?? 40;\n  const parkBelow = options.parkBelow ?? 0.02;\n  let energy = 0;\n  return {\n    get value() {\n      return energy;\n    },\n    bump(speed: number): number {\n      const target = Math.min(1, speed * gain);\n      energy =\n        target > energy\n          ? energy + (target - energy) * attack\n          : energy + (target - energy) * (1 - decay);\n      return energy;\n    },\n    parked(): boolean {\n      return energy < parkBelow;\n    },\n  };\n}\n"
    }
  ]
}