MobTranslate styleguide

Motion physics and transitions

Choose a motion relationship, distinguish duration curves from spring responses and preserve task state through interruption and reduced motion.

Two different motion contracts

A cubic-bezier easing curve and a damped spring answer different questions. A duration curve maps normalized time to progress; a spring describes the response to displacement and velocity. Do not label a finite easing token as a physics solver.

Material’s motion system includes scheme, spring and customization guidance. This library currently supplies duration/easing tokens for its components. The explorer below teaches an explicitly separate analytic model; it does not silently replace those component animations or implement native Material spring presets.

ModelInputsCompletion meaning
Duration and easingStart/end values, duration and progress curveA defined timeline reaches its endpoint
Spring responseInitial value/velocity, target, mass, stiffness and dampingA tolerance/horizon policy decides when the response is sufficiently settled
Task operationValidated data/state transitionActual completion or recovery, independent of presentation

Schemes, style, speed and application

Start with a coherent default scheme. Customize the scheme when the product’s tasks justify a different character; override an individual element only with a reason and an interruption/reduced-motion contract. A collection of unrelated bounces is not a system.

The model fields here are ordinary mathematical parameters. They are not the published numerical Material token values, Android stiffness constants or a cross-platform unit conversion.

DecisionDesign responsibilityLocal implementation boundary
Standard or expressive schemeChoose coherent motion character for the task and audienceRecord a deliberate scheme; do not infer one from a spring’s damping ratio
StyleDetermine whether restrained or more noticeable response helps orientationOvershoot is optional and may be unsuitable for reading or precise controls
SpeedBalance immediate response, continuity and understandable arrivalA faster response must not postpone cancellation or necessary feedback
Spatial applicationChanging position, size or relationshipsPreserve layout/focus and consider collision/overshoot
Effects applicationChanging opacity, color or emphasisDo not overshoot a bounded opacity or color representation without a valid mapping
Component overrideAn exception for a particular element/taskRecord purpose and verify it still belongs to the system

Spring mechanics and regimes

The unit-step model solves m·x″ + c·x′ + k·(x−1) = 0, starting at x=0 with the entered initial velocity. Its target is x=1. Positive mass and stiffness define the natural angular frequency √(k/m); damping ratio is c/(2√(km)). Time is in seconds and displacement is in model units.

Under-damping can cross the target and oscillate. Critical damping is the boundary between oscillatory and nonoscillatory behavior. Over-damping has two real decay rates and can approach the target slowly. Initial velocity matters in every regime; do not claim that damping alone prevents every target crossing. Zero damping retains ideal oscillation.

The implementation uses analytic under/critical/over-damped responses, ordinary floating-point arithmetic and a small relative tolerance around the critical boundary. That numerical branch is not a formal proof for arbitrary real-number inputs. The editor bounds keep this tool’s inputs and work finite.

ParameterChange to inspectConstraint
MassThe response changes with inertia relative to other parametersUse a consistent unit system; it is not a CSS length
StiffnessRestoring response grows relative to massA large value can require sufficient temporal sampling
DampingEnergy loss and response character changeDamping zero is not a settled animation
Initial velocityAn interrupted response may already be movingResetting it can create a discontinuity
HorizonA bounded observation/playback intervalEnding playback does not prove convergence

Captured response explorer

Capture a model, inspect its curve, regime, parameters and equivalent samples, and play the time cursor. Change draft settings while retaining the existing capture; a replay captures the new valid settings. Empty or out-of-bound fields cannot replace the captured model.

Stop retains the current time and curve. Reset moves the cursor to the captured start without rewriting draft parameters. Operating-system reduced motion displays the endpoint without cursor animation; enabling it during playback stops movement and preserves the response data.

The plot’s vertical scale adapts to the captured trajectory. It is not a component displacement in CSS pixels. Full parameters and all sampled values are available as JSON independently of the graphic. Displayed decimals are rounded for readability; the JSON retains the computed floating-point values.

This dimensionless unit-step model moves from x=0 toward x=1. Its parameters are model units, not Android spring tokens or a physical device calibration.

Draft model parameters

Mass 0.1–10; stiffness 1–1000; damping 0–100; initial velocity −5–5; preview 0.1–8 seconds. Draft changes do not rewrite the captured curve. An empty field is invalid.

Draft parameters are within the model bounds.

Reduced motion not requested: cursor playback available.

Response over 4 seconds; dashed line is target x=1. The vertical scale adapts to the captured samples. Equivalent values are below.

t=0.000 s; x=0.0000; velocity=0.0000 model units/s

Captured mass
1
Captured stiffness
100
Captured damping
20
Captured initial velocity
0
Captured preview seconds
4
Captured regime
critical
Damping ratio
1.0000
Natural angular frequency
10.0000 rad/s
Endpoint and velocity
x=1.0000; v=0.0000

Positive damping approaches the target, but this chosen horizon may end while displacement or velocity remains.

SecondsPositionVelocity
0.0000.0000000.000000
1.0000.9995010.004540
2.0001.0000000.000000
3.0001.0000000.000000
4.0001.0000000.000000
Full captured parameters and 241 samples
json
{
  "parameters": {
    "mass": 1,
    "stiffness": 100,
    "damping": 20,
    "initialVelocity": 0,
    "duration": 4
  },
  "samples": [
    {
      "time": 0,
      "position": 0,
      "velocity": 0
    },
    {
      "time": 0.016666666666666666,
      "position": 0.012437987627616942,
      "velocity": 1.410802874817691
    },
    {
      "time": 0.03333333333333333,
      "position": 0.04462491923494771,
      "velocity": 2.38843770191263
    },
    {
      "time": 0.05,
      "position": 0.09020401043104986,
      "velocity": 3.032653298563167
    },
    {
      "time": 0.06666666666666667,
      "position": 0.1443048016123467,
      "velocity": 3.4227807935506123
    },
    {
      "time": 0.08333333333333333,
      "position": 0.20323661773702328,
      "velocity": 3.621651737558985
    },
    {
      "time": 0.1,
      "position": 0.26424111765711533,
      "velocity": 3.6787944117144233
    },
    {
      "time": 0.11666666666666667,
      "position": 0.3252930148517049,
      "velocity": 3.6330376123369743
    },
    {
      "time": 0.13333333333333333,
      "position": 0.38494001106330433,
      "velocity": 3.514628508209689
    },
    {
      "time": 0.15,
      "position": 0.44217459962892547,
      "velocity": 3.346952402226447
    },
    {
      "time": 0.16666666666666666,
      "position": 0.49633172576650164,
      "velocity": 3.147926713959364
    },
    {
      "time": 0.18333333333333332,
      "position": 0.547007386107534,
      "velocity": 2.931128678127721
    },
    {
      "time": 0.2,
      "position": 0.5939941502901619,
      "velocity": 2.706705664732254
    },
    {
      "time": 0.21666666666666667,
      "position": 0.637230327356489,
      "velocity": 2.482108286508234
    },
    {
      "time": 0.23333333333333334,
      "position": 0.6767601071186498,
      "velocity": 2.2626792501694513
    },
    {
      "time": 0.25,
      "position": 0.7127025048163542,
      "velocity": 2.05212496559747
    },
    {
      "time": 0.26666666666666666,
      "position": 0.7452273455163944,
      "velocity": 1.852892032608041
    },
    {
      "time": 0.2833333333333333,
      "position": 0.7745368587040187,
      "velocity": 1.666466696535514
    },
    {
      "time": 0.3,
      "position": 0.8008517265285442,
      "velocity": 1.4936120510359183
    },
    {
      "time": 0.31666666666666665,
      "position": 0.8244006520446816,
      "velocity": 1.334555044460419
    },
    {
      "time": 0.3333333333333333,
      "position": 0.8454126954952396,
      "velocity": 1.18913311157508
    },
    {
      "time": 0.35,
      "position": 0.8641117745995668,
      "velocity": 1.0569084197811476
    },
    {
      "time": 0.36666666666666664,
      "position": 0.8807128450362988,
      "velocity": 0.9372562175719379
    },
    {
      "time": 0.38333333333333336,
      "position": 0.8954193748557834,
      "velocity": 0.8294325442472353
    },
    {
      "time": 0.4,
      "position": 0.9084218055563291,
      "velocity": 0.7326255555493671
    },
    {
      "time": 0.4166666666666667,
      "position": 0.9198967564051186,
      "velocity": 0.6459938999587215
    },
    {
      "time": 0.43333333333333335,
      "position": 0.9300067800696482,
      "velocity": 0.5686949119341081
    },
    {
      "time": 0.45,
      "position": 0.9389005190396673,
      "velocity": 0.49990484422090375
    },
    {
      "time": 0.4666666666666667,
      "position": 0.9467131455415272,
      "velocity": 0.43883291906977634
    },
    {
      "time": 0.48333333333333334,
      "position": 0.9535669942158791,
      "velocity": 0.38473061935414504
    },
    {
      "time": 0.5,
      "position": 0.9595723180054871,
      "velocity": 0.33689734995427334
    },
    {
      "time": 0.5166666666666667,
      "position": 0.9648281145122877,
      "velocity": 0.2946833648970491
    },
    {
      "time": 0.5333333333333333,
      "position": 0.9694229833724008,
      "velocity": 0.25749066633767687
    },
    {
      "time": 0.55,
      "position": 0.9734359856499836,
      "velocity": 0.22477242911552367
    },
    {
      "time": 0.5666666666666667,
      "position": 0.9769374844235683,
      "velocity": 0.19603138239966977
    },
    {
      "time": 0.5833333333333334,
      "position": 0.9799899520854091,
      "velocity": 0.17081748219772755
    },
    {
      "time": 0.6,
      "position": 0.9826487347633355,
      "velocity": 0.14872513059998152
    },
    {
      "time": 0.6166666666666667,
      "position": 0.9849627680037535,
      "velocity": 0.12939013578165567
    },
    {
      "time": 0.6333333333333333,
      "position": 0.9869752406646145,
      "velocity": 0.11248655789651067
    },
    {
      "time": 0.65,
      "position": 0.9887242060526682,
      "velocity": 0.0977235475435422
    },
    {
      "time": 0.6666666666666666,
      "position": 0.9902431408563948,
      "velocity": 0.08484225342265393
    },
    {
      "time": 0.6833333333333333,
      "position": 0.9915614535000543,
      "velocity": 0.073612852446335
    },
    {
      "time": 0.7,
      "position": 0.9927049442755639,
      "velocity": 0.06383173758881613
    },
    {
      "time": 0.7166666666666667,
      "position": 0.9936962200773563,
      "velocity": 0.05531888503544485
    },
    {
      "time": 0.7333333333333333,
      "position": 0.9945550668344385,
      "velocity": 0.04791541185694123
    },
    {
      "time": 0.75,
      "position": 0.9952987828537434,
      "velocity": 0.041481327761087525
    },
    {
      "time": 0.7666666666666667,
      "position": 0.9959424762990092,
      "velocity": 0.03589347889337957
    },
    {
      "time": 0.7833333333333333,
      "position": 0.9964993299607008,
      "velocity": 0.031043677706992633
    },
    {
      "time": 0.8,
      "position": 0.9969808363488774,
      "velocity": 0.026837010232200947
    },
    {
      "time": 0.8166666666666667,
      "position": 0.9973970059808868,
      "velocity": 0.02319031035209929
    },
    {
      "time": 0.8333333333333334,
      "position": 0.9977565515534179,
      "velocity": 0.020030789701626174
    },
    {
      "time": 0.85,
      "position": 0.9980670504943989,
      "velocity": 0.017294811365904755
    },
    {
      "time": 0.8666666666666667,
      "position": 0.9983350881923788,
      "velocity": 0.014926795516603523
    },
    {
      "time": 0.8833333333333333,
      "position": 0.9985663840051096,
      "velocity": 0.012878245377829906
    },
    {
      "time": 0.9,
      "position": 0.9987659019591332,
      "velocity": 0.011106882367801161
    },
    {
      "time": 0.9166666666666666,
      "position": 0.9989379478710443,
      "velocity": 0.009575879851239652
    },
    {
      "time": 0.9333333333333333,
      "position": 0.9990862544505146,
      "velocity": 0.00825318560825575
    },
    {
      "time": 0.95,
      "position": 0.9992140557861792,
      "velocity": 0.007110923839331556
    },
    {
      "time": 0.9666666666666667,
      "position": 0.999324152468538,
      "velocity": 0.006124868253874099
    },
    {
      "time": 0.9833333333333333,
      "position": 0.999418968469177,
      "velocity": 0.005273978510547581
    },
    {
      "time": 1,
      "position": 0.9995006007726127,
      "velocity": 0.0045399929762484845
    },
    {
      "time": 1.0166666666666666,
      "position": 0.9995708626454496,
      "velocity": 0.003907071436951528
    },
    {
      "time": 1.0333333333333334,
      "position": 0.9996313213267097,
      "velocity": 0.003361482021176028
    },
    {
      "time": 1.05,
      "position": 0.999683330832478,
      "velocity": 0.002891327181723452
    },
    {
      "time": 1.0666666666666667,
      "position": 0.9997280604866657,
      "velocity": 0.0024863041219132815
    },
    {
      "time": 1.0833333333333333,
      "position": 0.9997665197169967,
      "velocity": 0.002137495548621671
    },
    {
      "time": 1.1,
      "position": 0.999799579590517,
      "velocity": 0.0018371870869270225
    },
    {
      "time": 1.1166666666666667,
      "position": 0.9998279915053286,
      "velocity": 0.0015787081017779704
    },
    {
      "time": 1.1333333333333333,
      "position": 0.999852403404142,
      "velocity": 0.0013562930430185496
    },
    {
      "time": 1.15,
      "position": 0.9998733738300171,
      "velocity": 0.0011649607638425318
    },
    {
      "time": 1.1666666666666667,
      "position": 0.9998913841046995,
      "velocity": 0.001000409561978433
    },
    {
      "time": 1.1833333333333333,
      "position": 0.9999068488747217,
      "velocity": 0.0008589259603580048
    },
    {
      "time": 1.2,
      "position": 0.9999201252394068,
      "velocity": 0.0007373054823993851
    },
    {
      "time": 1.2166666666666666,
      "position": 0.9999315206476326,
      "velocity": 0.000632783888964304
    },
    {
      "time": 1.2333333333333334,
      "position": 0.999941299726267,
      "velocity": 0.0005429775320301175
    },
    {
      "time": 1.25,
      "position": 0.9999496901821769,
      "velocity": 0.0004658316465098339
    },
    {
      "time": 1.2666666666666666,
      "position": 0.9999568879013293,
      "velocity": 0.00039957554865489625
    },
    {
      "time": 1.2833333333333334,
      "position": 0.9999630613524066,
      "velocity": 0.0003426838391185822
    },
    {
      "time": 1.3,
      "position": 0.9999683553883023,
      "velocity": 0.00029384282290753703
    },
    {
      "time": 1.3166666666666667,
      "position": 0.9999728945265841,
      "velocity": 0.00025192145880690317
    },
    {
      "time": 1.3333333333333333,
      "position": 0.9999767857793102,
      "velocity": 0.000215946238975015
    },
    {
      "time": 1.35,
      "position": 0.9999801210932474,
      "velocity": 0.00018507947666185137
    },
    {
      "time": 1.3666666666666667,
      "position": 0.9999829794534211,
      "velocity": 0.00015860054766690405
    },
    {
      "time": 1.3833333333333333,
      "position": 0.9999854286958548,
      "velocity": 0.000135889690342527
    },
    {
      "time": 1.4,
      "position": 0.9999875270692135,
      "velocity": 0.00011641402067449951
    },
    {
      "time": 1.4166666666666667,
      "position": 0.9999893245797227,
      "velocity": 0.00009971546412940662
    },
    {
      "time": 1.4333333333333333,
      "position": 0.9999908641491025,
      "velocity": 0.00008540034534599335
    },
    {
      "time": 1.45,
      "position": 0.9999921826112302,
      "velocity": 0.00007313041107234377
    },
    {
      "time": 1.4666666666666666,
      "position": 0.9999933115697568,
      "velocity": 0.0000626150916387356
    },
    {
      "time": 1.4833333333333334,
      "position": 0.9999942781358823,
      "velocity": 0.000053604832260615056
    },
    {
      "time": 1.5,
      "position": 0.999995105562872,
      "velocity": 0.00004588534807527387
    },
    {
      "time": 1.5166666666666666,
      "position": 0.9999958137916302,
      "velocity": 0.000039272676459141435
    },
    {
      "time": 1.5333333333333334,
      "position": 0.9999964199196866,
      "velocity": 0.00003360891722789959
    },
    {
      "time": 1.55,
      "position": 0.9999969386042517,
      "velocity": 0.000028758566120547662
    },
    {
      "time": 1.5666666666666667,
      "position": 0.9999973824085318,
      "velocity": 0.000024605359801931022
    },
    {
      "time": 1.5833333333333333,
      "position": 0.9999977620992254,
      "velocity": 0.000021049561740806517
    },
    {
      "time": 1.6,
      "position": 0.9999980869020297,
      "velocity": 0.00001800562795508146
    },
    {
      "time": 1.6166666666666667,
      "position": 0.9999983647210354,
      "velocity": 0.00001540019995716804
    },
    {
      "time": 1.6333333333333333,
      "position": 0.9999986023270787,
      "velocity": 0.000013170379450449392
    },
    {
      "time": 1.65,
      "position": 0.9999988055194091,
      "velocity": 0.000011262245570952535
    },
    {
      "time": 1.6666666666666667,
      "position": 0.9999989792644283,
      "velocity": 0.000009629580865698555
    },
    {
      "time": 1.6833333333333333,
      "position": 0.9999991278147283,
      "velocity": 0.000008232776863381885
    },
    {
      "time": 1.7,
      "position": 0.9999992548112107,
      "velocity": 0.000007037894121934784
    },
    {
      "time": 1.7166666666666666,
      "position": 0.9999993633706722,
      "velocity": 0.000006015855116285164
    },
    {
      "time": 1.7333333333333334,
      "position": 0.9999994561609168,
      "velocity": 0.000005141751332180654
    },
    {
      "time": 1.75,
      "position": 0.9999995354651562,
      "velocity": 0.000004394248522551968
    },
    {
      "time": 1.7666666666666666,
      "position": 0.9999996032372193,
      "velocity": 0.0000037550763172437145
    },
    {
      "time": 1.7833333333333334,
      "position": 0.9999996611488745,
      "velocity": 0.000003208590303381589
    },
    {
      "time": 1.8,
      "position": 0.9999997106303848,
      "velocity": 0.000002741396354048273
    },
    {
      "time": 1.8166666666666667,
      "position": 0.9999997529052591,
      "velocity": 0.000002342028413590876
    },
    {
      "time": 1.8333333333333333,
      "position": 0.9999997890200246,
      "velocity": 0.000002000672180190056
    },
    {
      "time": 1.85,
      "position": 0.9999998198697316,
      "velocity": 0.00000170892818746456
    },
    {
      "time": 1.8666666666666667,
      "position": 0.9999998462197977,
      "velocity": 0.000001459608700378282
    },
    {
      "time": 1.8833333333333333,
      "position": 0.9999998687247154,
      "velocity": 0.0000012465636268826435
    },
    {
      "time": 1.9,
      "position": 0.9999998879440712,
      "velocity": 0.000001064531323132081
    },
    {
      "time": 1.9166666666666667,
      "position": 0.99999990435626,
      "velocity": 9.090107519260264e-7
    },
    {
      "time": 1.9333333333333333,
      "position": 0.9999999183702255,
      "velocity": 7.761519543582365e-7
    },
    {
      "time": 1.95,
      "position": 0.9999999303355097,
      "velocity": 6.626622248015388e-7
    },
    {
      "time": 1.9666666666666666,
      "position": 0.9999999405508535,
      "velocity": 5.657257490790038e-7
    },
    {
      "time": 1.9833333333333334,
      "position": 0.9999999492715563,
      "velocity": 4.82934783386067e-7
    },
    {
      "time": 2,
      "position": 0.9999999567157739,
      "velocity": 4.122307244877116e-7
    },
    {
      "time": 2.0166666666666666,
      "position": 0.9999999630699055,
      "velocity": 3.518536561732436e-7
    },
    {
      "time": 2.033333333333333,
      "position": 0.999999968493203,
      "velocity": 3.0029915829722897e-7
    },
    {
      "time": 2.05,
      "position": 0.9999999731217134,
      "velocity": 2.5628133760928223e-7
    },
    {
      "time": 2.066666666666667,
      "position": 0.9999999770716497,
      "velocity": 2.1870118797463882e-7
    },
    {
      "time": 2.0833333333333335,
      "position": 0.9999999804422748,
      "velocity": 1.8661951495201458e-7
    },
    {
      "time": 2.1,
      "position": 0.999999983318367,
      "velocity": 1.5923376898615005e-7
    },
    {
      "time": 2.1166666666666667,
      "position": 0.9999999857723276,
      "velocity": 1.358582252372474e-7
    },
    {
      "time": 2.1333333333333333,
      "position": 0.999999987865983,
      "velocity": 1.1590702849955578e-7
    },
    {
      "time": 2.15,
      "position": 0.9999999896521254,
      "velocity": 9.88796906410248e-8
    },
    {
      "time": 2.1666666666666665,
      "position": 0.9999999911758296,
      "velocity": 8.434868714543002e-8
    },
    {
      "time": 2.183333333333333,
      "position": 0.9999999924755782,
      "velocity": 7.194885005010016e-8
    },
    {
      "time": 2.2,
      "position": 0.9999999935842234,
      "velocity": 6.136829804311634e-8
    },
    {
      "time": 2.216666666666667,
      "position": 0.9999999945298085,
      "velocity": 5.2340681741601525e-8
    },
    {
      "time": 2.2333333333333334,
      "position": 0.9999999953362705,
      "velocity": 4.463855410064601e-8
    },
    {
      "time": 2.25,
      "position": 0.9999999960240399,
      "velocity": 3.806770325884043e-8
    },
    {
      "time": 2.2666666666666666,
      "position": 0.999999996610553,
      "velocity": 3.246230856247972e-8
    },
    {
      "time": 2.283333333333333,
      "position": 0.9999999971106902,
      "velocity": 2.768080058520724e-8
    },
    {
      "time": 2.3,
      "position": 0.9999999975371489,
      "velocity": 2.3602323152914347e-8
    },
    {
      "time": 2.316666666666667,
      "position": 0.9999999979007641,
      "velocity": 2.012371010675897e-8
    },
    {
      "time": 2.3333333333333335,
      "position": 0.9999999982107802,
      "velocity": 1.715690214339223e-8
    },
    {
      "time": 2.35,
      "position": 0.9999999984750846,
      "velocity": 1.4626739863833289e-8
    },
    {
      "time": 2.3666666666666667,
      "position": 0.9999999987004059,
      "velocity": 1.246907840073669e-8
    },
    {
      "time": 2.3833333333333333,
      "position": 0.9999999988924844,
      "velocity": 1.0629176900849464e-8
    },
    {
      "time": 2.4,
      "position": 0.9999999990562164,
      "velocity": 9.060322906269834e-9
    },
    {
      "time": 2.4166666666666665,
      "position": 0.9999999991957784,
      "velocity": 7.722657468444174e-9
    },
    {
      "time": 2.433333333333333,
      "position": 0.9999999993147328,
      "velocity": 6.582171783004745e-9
    },
    {
      "time": 2.45,
      "position": 0.9999999994161176,
      "velocity": 5.609850371831605e-9
    },
    {
      "time": 2.466666666666667,
      "position": 0.9999999995025238,
      "velocity": 4.780939464154334e-9
    },
    {
      "time": 2.4833333333333334,
      "position": 0.9999999995761611,
      "velocity": 4.074322329378578e-9
    },
    {
      "time": 2.5,
      "position": 0.9999999996389135,
      "velocity": 3.4719859662410053e-9
    },
    {
      "time": 2.5166666666666666,
      "position": 0.9999999996923875,
      "velocity": 2.958565820629696e-9
    },
    {
      "time": 2.533333333333333,
      "position": 0.9999999997379532,
      "velocity": 2.5209571434136007e-9
    },
    {
      "time": 2.55,
      "position": 0.9999999997767782,
      "velocity": 2.147983257389505e-9
    },
    {
      "time": 2.566666666666667,
      "position": 0.9999999998098584,
      "velocity": 1.830112419631953e-9
    },
    {
      "time": 2.5833333333333335,
      "position": 0.9999999998380427,
      "velocity": 1.559216176919723e-9
    },
    {
      "time": 2.6,
      "position": 0.9999999998620546,
      "velocity": 1.3283631472964645e-9
    },
    {
      "time": 2.6166666666666667,
      "position": 0.999999999882511,
      "velocity": 1.131643045693126e-9
    },
    {
      "time": 2.6333333333333333,
      "position": 0.9999999998999375,
      "velocity": 9.640165277073149e-10
    },
    {
      "time": 2.65,
      "position": 0.9999999999147825,
      "velocity": 8.211870717612837e-10
    },
    {
      "time": 2.6666666666666665,
      "position": 0.9999999999274277,
      "velocity": 6.994916719118143e-10
    },
    {
      "time": 2.683333333333333,
      "position": 0.9999999999381989,
      "velocity": 5.958075852176972e-10
    },
    {
      "time": 2.7,
      "position": 0.9999999999473732,
      "velocity": 5.074727804655524e-10
    },
    {
      "time": 2.716666666666667,
      "position": 0.9999999999551872,
      "velocity": 4.3221807920119237e-10
    },
    {
      "time": 2.7333333333333334,
      "position": 0.9999999999618423,
      "velocity": 3.681092739547595e-10
    },
    {
      "time": 2.75,
      "position": 0.9999999999675102,
      "velocity": 3.1349775958719774e-10
    },
    {
      "time": 2.7666666666666666,
      "position": 0.9999999999723371,
      "velocity": 2.6697842806825847e-10
    },
    {
      "time": 2.783333333333333,
      "position": 0.9999999999764477,
      "velocity": 2.2735376006064614e-10
    },
    {
      "time": 2.8,
      "position": 0.9999999999799483,
      "velocity": 1.9360320299432567e-10
    },
    {
      "time": 2.816666666666667,
      "position": 0.999999999982929,
      "velocity": 1.6485705876983542e-10
    },
    {
      "time": 2.8333333333333335,
      "position": 0.9999999999854672,
      "velocity": 1.403742181629597e-10
    },
    {
      "time": 2.85,
      "position": 0.9999999999876283,
      "velocity": 1.1952317626381702e-10
    },
    {
      "time": 2.8666666666666667,
      "position": 0.9999999999894684,
      "velocity": 1.0176584630532461e-10
    },
    {
      "time": 2.8833333333333333,
      "position": 0.9999999999910352,
      "velocity": 8.664376009871404e-11
    },
    {
      "time": 2.9,
      "position": 0.999999999992369,
      "velocity": 7.376630377393076e-11
    },
    {
      "time": 2.9166666666666665,
      "position": 0.9999999999935046,
      "velocity": 6.28006891381515e-11
    },
    {
      "time": 2.933333333333333,
      "position": 0.9999999999944714,
      "velocity": 5.346340501264443e-11
    },
    {
      "time": 2.95,
      "position": 0.9999999999952944,
      "velocity": 4.5512930494160694e-11
    },
    {
      "time": 2.966666666666667,
      "position": 0.9999999999959951,
      "velocity": 3.874352415760013e-11
    },
    {
      "time": 2.9833333333333334,
      "position": 0.9999999999965915,
      "velocity": 3.297993057949833e-11
    },
    {
      "time": 3,
      "position": 0.9999999999970991,
      "velocity": 2.8072868906520524e-11
    },
    {
      "time": 3.0166666666666666,
      "position": 0.9999999999975313,
      "velocity": 2.389518810847863e-11
    },
    {
      "time": 3.033333333333333,
      "position": 0.9999999999978991,
      "velocity": 2.0338590544145965e-11
    },
    {
      "time": 3.05,
      "position": 0.9999999999982122,
      "velocity": 1.7310839959529804e-11
    },
    {
      "time": 3.066666666666667,
      "position": 0.9999999999984787,
      "velocity": 1.4733382398675727e-11
    },
    {
      "time": 3.0833333333333335,
      "position": 0.9999999999987054,
      "velocity": 1.253931904927307e-11
    },
    {
      "time": 3.1,
      "position": 0.9999999999988984,
      "velocity": 1.0671679036256928e-11
    },
    {
      "time": 3.1166666666666667,
      "position": 0.9999999999990626,
      "velocity": 9.081947844100996e-12
    },
    {
      "time": 3.1333333333333333,
      "position": 0.9999999999992024,
      "velocity": 7.728813587010423e-12
    },
    {
      "time": 3.15,
      "position": 0.9999999999993214,
      "velocity": 6.5770989216846905e-12
    },
    {
      "time": 3.1666666666666665,
      "position": 0.9999999999994227,
      "velocity": 5.596851151326639e-12
    },
    {
      "time": 3.183333333333333,
      "position": 0.9999999999995088,
      "velocity": 4.7625671229337815e-12
    },
    {
      "time": 3.2,
      "position": 0.9999999999995821,
      "velocity": 4.052532975710136e-12
    },
    {
      "time": 3.216666666666667,
      "position": 0.9999999999996445,
      "velocity": 3.4482617446190228e-12
    },
    {
      "time": 3.2333333333333334,
      "position": 0.9999999999996976,
      "velocity": 2.934014334689795e-12
    },
    {
      "time": 3.25,
      "position": 0.9999999999997427,
      "velocity": 2.496391522690681e-12
    },
    {
      "time": 3.2666666666666666,
      "position": 0.9999999999997811,
      "velocity": 2.123986467781465e-12
    },
    {
      "time": 3.283333333333333,
      "position": 0.9999999999998138,
      "velocity": 1.8070887683251036e-12
    },
    {
      "time": 3.3,
      "position": 0.9999999999998416,
      "velocity": 1.5374324278841213e-12
    },
    {
      "time": 3.316666666666667,
      "position": 0.9999999999998652,
      "velocity": 1.3079812234250699e-12
    },
    {
      "time": 3.3333333333333335,
      "position": 0.9999999999998854,
      "velocity": 1.112745931788333e-12
    },
    {
      "time": 3.35,
      "position": 0.9999999999999025,
      "velocity": 9.466286911837297e-13
    },
    {
      "time": 3.3666666666666667,
      "position": 0.9999999999999171,
      "velocity": 8.052904738483495e-13
    },
    {
      "time": 3.3833333333333333,
      "position": 0.9999999999999295,
      "velocity": 6.850382419616285e-13
    },
    {
      "time": 3.4,
      "position": 0.99999999999994,
      "velocity": 5.827288667242844e-13
    },
    {
      "time": 3.4166666666666665,
      "position": 0.9999999999999489,
      "velocity": 4.956873231907199e-13
    },
    {
      "time": 3.433333333333333,
      "position": 0.9999999999999566,
      "velocity": 4.2163704209865333e-13
    },
    {
      "time": 3.45,
      "position": 0.9999999999999631,
      "velocity": 3.5864061402622656e-13
    },
    {
      "time": 3.466666666666667,
      "position": 0.9999999999999686,
      "velocity": 3.0504930879209445e-13
    },
    {
      "time": 3.4833333333333334,
      "position": 0.9999999999999734,
      "velocity": 2.594601009728445e-13
    },
    {
      "time": 3.5,
      "position": 0.9999999999999774,
      "velocity": 2.2067908660514462e-13
    },
    {
      "time": 3.5166666666666666,
      "position": 0.9999999999999807,
      "velocity": 1.8769034156193613e-13
    },
    {
      "time": 3.533333333333333,
      "position": 0.9999999999999836,
      "velocity": 1.5962941299990137e-13
    },
    {
      "time": 3.55,
      "position": 0.999999999999986,
      "velocity": 1.3576075529744831e-13
    },
    {
      "time": 3.566666666666667,
      "position": 0.9999999999999881,
      "velocity": 1.1545852413032922e-13
    },
    {
      "time": 3.5833333333333335,
      "position": 0.9999999999999899,
      "velocity": 9.819022940056385e-14
    },
    {
      "time": 3.6,
      "position": 0.9999999999999915,
      "velocity": 8.350282188876851e-14
    },
    {
      "time": 3.6166666666666667,
      "position": 0.9999999999999927,
      "velocity": 7.101085165334993e-14
    },
    {
      "time": 3.6333333333333333,
      "position": 0.9999999999999938,
      "velocity": 6.038638998238894e-14
    },
    {
      "time": 3.65,
      "position": 0.9999999999999947,
      "velocity": 5.1350452504284355e-14
    },
    {
      "time": 3.6666666666666665,
      "position": 0.9999999999999956,
      "velocity": 4.366570006457927e-14
    },
    {
      "time": 3.683333333333333,
      "position": 0.9999999999999962,
      "velocity": 3.713022718699053e-14
    },
    {
      "time": 3.7,
      "position": 0.9999999999999968,
      "velocity": 3.157227621525304e-14
    },
    {
      "time": 3.716666666666667,
      "position": 0.9999999999999972,
      "velocity": 2.6845739310623742e-14
    },
    {
      "time": 3.7333333333333334,
      "position": 0.9999999999999977,
      "velocity": 2.2826330980928316e-14
    },
    {
      "time": 3.75,
      "position": 0.999999999999998,
      "velocity": 1.9408331271757006e-14
    },
    {
      "time": 3.7666666666666666,
      "position": 0.9999999999999983,
      "velocity": 1.6501814610974977e-14
    },
    {
      "time": 3.783333333333333,
      "position": 0.9999999999999986,
      "velocity": 1.403029194924417e-14
    },
    {
      "time": 3.8,
      "position": 0.9999999999999988,
      "velocity": 1.1928704609782512e-14
    },
    {
      "time": 3.816666666666667,
      "position": 0.999999999999999,
      "velocity": 1.0141717429473833e-14
    },
    {
      "time": 3.8333333333333335,
      "position": 0.9999999999999991,
      "velocity": 8.622266578613423e-15
    },
    {
      "time": 3.85,
      "position": 0.9999999999999992,
      "velocity": 7.330324090652841e-15
    },
    {
      "time": 3.8666666666666667,
      "position": 0.9999999999999993,
      "velocity": 6.231846788835621e-15
    },
    {
      "time": 3.8833333333333333,
      "position": 0.9999999999999994,
      "velocity": 5.297882110529085e-15
    },
    {
      "time": 3.9,
      "position": 0.9999999999999996,
      "velocity": 4.503807427476157e-15
    },
    {
      "time": 3.9166666666666665,
      "position": 0.9999999999999996,
      "velocity": 3.828682947647515e-15
    },
    {
      "time": 3.933333333333333,
      "position": 0.9999999999999997,
      "velocity": 3.2547012525864414e-15
    },
    {
      "time": 3.95,
      "position": 0.9999999999999997,
      "velocity": 2.7667190503366147e-15
    },
    {
      "time": 3.966666666666667,
      "position": 0.9999999999999998,
      "velocity": 2.3518588739914028e-15
    },
    {
      "time": 3.9833333333333334,
      "position": 0.9999999999999998,
      "velocity": 1.9991702855839287e-15
    },
    {
      "time": 4,
      "position": 0.9999999999999998,
      "velocity": 1.6993417021166355e-15
    }
  ]
}

Initial captured model. Change settings and run to capture another response.

Playback inactive. The curve is sampled at up to 60 intervals per second; the cursor evaluates the captured analytic response at elapsed browser time. This plot does not animate a production component, preserve retargeted velocity or measure frame-rate performance.

Settling and duration selection

Choose a settling policy from the property and perceptual context: residual displacement, residual velocity and a maximum observation interval have different jobs. A timer reaching four seconds is not sufficient evidence that a spring settled. Measure the final values and make the stopping rule explicit.

For duration/easing transitions, use the committed token scale and a suitable entering, exiting or continuing curve. Distance, context and repetition affect what feels understandable; do not create one arbitrary duration for every overlay and navigation change. Use the existing Motion chapter’s actual token tables and replay lab.

  • Keep the actual data transition independent of animation-end events.
  • Avoid hiding useful results while an optional flourish finishes.
  • Compare repeated actions, slow hardware, enlarged text and reduced motion.
  • For a production spring renderer, define sampling/integration, completion tolerances and behavior when the tab is backgrounded.
  • This explorer samples its curve at up to 60 intervals per second and evaluates the cursor at elapsed browser time; it does not certify a display frame rate.

Transition patterns and identity

Choose a pattern from the relationship rather than drawing every arrival from a screen edge. A decorative morph must not imply two records are the same object. Coordinate entering and exiting regions without leaving two competing primary tasks or a focusable hidden surface.

Navigation, Dialog/Drawer, Layout compositions and Skeleton examples supply working task/state patterns. Applying an animation still requires their existing semantics and focus contract; motion cannot repair an inaccessible interaction model.

PatternUsePreserve
Container transformA clearly related object expands into a detailed contextObject identity, readable labels and a usable return path
Forward/backwardA task moves deeper into or returns through a hierarchyActual history/destination truth and focus restoration
LateralSibling activities change within one levelSelected destination and shared context
Top-levelIndependent primary destinations changeConsistent navigation, current location and relevant retained state
Enter/exit within boundsA contained surface or item appears/disappearsReading order and the associated control relationship
Enter/exit beyond boundsA temporary surface originates outside the current regionSafe area, unobscured focus and reachable dismissal
Skeleton replacementKnown content structure waits for real dataStable geometry and truthful loading/empty/error outcomes

Interruption and retargeting

  • Give the latest action authority. Cancel or supersede presentation work without duplicating the operation.
  • A retargeted spring needs the current value and velocity in a consistent coordinate system if continuity is intended. Restarting from zero is a different behavior.
  • When geometry changes, define how a model value maps to the new surface. A changing plot scale is not proof of a continuous pixel trajectory.
  • Keep element identity stable while choosing new targets; do not attach a previous response to an unrelated record.
  • Restore focus when a region disappears and keep cancellation reachable while presentation is active.
  • The existing duration lab cancels/replays a visual transition. This spring explorer replaces a captured unit-step playback; it does not implement continuous component retargeting.

Reduced motion and accessibility

Keep the information and task outcome available without movement. Reduced motion is not a reason to remove the selected state, result or recovery operation. Avoid large-field zoom, parallax and compulsory oscillation around reading content.

Check the preference at the beginning and respond to changes while playing. A CSS media query cannot cancel an unrelated JavaScript frame loop by itself. Provide a reachable stop path for optional ongoing playback and avoid announcing every visual frame.

  • The plot is decorative because model characteristics, samples and full JSON provide the equivalent information.
  • Cursor readout updates without an automatic live channel; the status describes explicit start/stop/reset/end events.
  • A preview horizon is bounded to eight seconds, and replay supersedes the old frame callback.
  • No preference changes rewrite the captured source values or establish a physical performance claim.

Analytic model source

typescript
export interface SpringParameters {
  mass:number;
  stiffness:number;
  damping:number;
  initialVelocity:number;
  duration:number;
}
export interface SpringSample {time:number;position:number;velocity:number}
export const initialSpring:SpringParameters={mass:1,stiffness:100,damping:20,initialVelocity:0,duration:4};

export function validateSpring(parameters:SpringParameters):string[]{
  const errors:string[]=[];
  if(!Number.isFinite(parameters.mass)||parameters.mass<.1||parameters.mass>10)errors.push('Mass must be 0.1–10.');
  if(!Number.isFinite(parameters.stiffness)||parameters.stiffness<1||parameters.stiffness>1000)errors.push('Stiffness must be 1–1000.');
  if(!Number.isFinite(parameters.damping)||parameters.damping<0||parameters.damping>100)errors.push('Damping must be 0–100.');
  if(!Number.isFinite(parameters.initialVelocity)||Math.abs(parameters.initialVelocity)>5)errors.push('Initial velocity must be −5–5.');
  if(!Number.isFinite(parameters.duration)||parameters.duration<.1||parameters.duration>8)errors.push('Preview duration must be 0.1–8 seconds.');
  return errors;
}

export function springCharacteristics(parameters:SpringParameters){
  const errors=validateSpring(parameters);if(errors.length)throw new RangeError(errors.join(' '));
  const naturalFrequency=Math.sqrt(parameters.stiffness/parameters.mass);
  const alpha=parameters.damping/(2*parameters.mass);
  const discriminant=alpha*alpha-naturalFrequency*naturalFrequency;
  const tolerance=1e-10*Math.max(1,alpha*alpha,naturalFrequency*naturalFrequency);
  const regime=Math.abs(discriminant)<=tolerance?'critical':discriminant<0?'underdamped':'overdamped';
  return {naturalFrequency,alpha,discriminant,regime,dampingRatio:alpha/naturalFrequency};
}

// Unit-step response of m*x'' + c*x' + k*(x-1) = 0, x(0)=0.
// Time is seconds, and the initial velocity is in model displacement units/second.
export function springResponse(parameters:SpringParameters,time:number):SpringSample{
  const {naturalFrequency,alpha,discriminant,regime}=springCharacteristics(parameters);
  if(!Number.isFinite(time)||time<0||time>parameters.duration)throw new RangeError('Time must be within the captured preview duration.');
  const displacement=-1,initialVelocity=parameters.initialVelocity;
  let offset:number,velocity:number;
  if(regime==='critical'){
    const coefficient=initialVelocity+alpha*displacement;
    const decay=Math.exp(-alpha*time);
    offset=decay*(displacement+coefficient*time);
    velocity=decay*(coefficient-alpha*(displacement+coefficient*time));
  }else if(regime==='underdamped'){
    const frequency=Math.sqrt(-discriminant),coefficient=(initialVelocity+alpha*displacement)/frequency;
    const cosine=Math.cos(frequency*time),sine=Math.sin(frequency*time),decay=Math.exp(-alpha*time);
    offset=decay*(displacement*cosine+coefficient*sine);
    velocity=decay*((-alpha*displacement+coefficient*frequency)*cosine+(-alpha*coefficient-displacement*frequency)*sine);
  }else{
    const root=Math.sqrt(discriminant),fast=-alpha-root;
    // This equivalent slow root avoids subtracting nearly equal positive values.
    const slow=-(naturalFrequency*naturalFrequency)/(alpha+root);
    const first=(initialVelocity-slow*displacement)/(fast-slow),second=displacement-first;
    offset=first*Math.exp(fast*time)+second*Math.exp(slow*time);
    velocity=first*fast*Math.exp(fast*time)+second*slow*Math.exp(slow*time);
  }
  return {time,position:1+offset,velocity};
}

export function sampleSpring(parameters:SpringParameters):SpringSample[]{
  const errors=validateSpring(parameters);if(errors.length)throw new RangeError(errors.join(' '));
  const intervals=Math.floor(parameters.duration*60);
  return Array.from({length:intervals+1},(_,index)=>springResponse(parameters,index===intervals?parameters.duration:index/intervals*parameters.duration));
}

Compiled explorer source

tsx
'use client';
import {useEffect,useId,useRef,useState} from 'react';
import {Button} from '@mobtranslate/ui';
import {CodeBlock} from '../components/code-block';
import {initialSpring,sampleSpring,springCharacteristics,springResponse,validateSpring,type SpringParameters} from '../lib/spring-response';
import './spring-response.css';

const fields=[
  {key:'mass',label:'Mass',min:.1,max:10,step:.1},
  {key:'stiffness',label:'Stiffness',min:1,max:1000,step:1},
  {key:'damping',label:'Damping',min:0,max:100,step:1},
  {key:'initialVelocity',label:'Initial velocity',min:-5,max:5,step:.1},
  {key:'duration',label:'Preview seconds',min:.1,max:8,step:.1},
] as const;
const makeModel=(parameters:SpringParameters)=>{
  const samples=sampleSpring(parameters);
  const minimum=Math.min(0,...samples.map(sample=>sample.position)),maximum=Math.max(1,...samples.map(sample=>sample.position));
  const padding=(maximum-minimum)*.1;
  return {parameters:{...parameters},samples,minimum:minimum-padding,maximum:maximum+padding,characteristics:springCharacteristics(parameters)};
};

export function SpringResponseExample(){
  const id=useId();
  const [editor,setEditor]=useState<Record<keyof SpringParameters,string>>({mass:'1',stiffness:'100',damping:'20',initialVelocity:'0',duration:'4'});
  const [model,setModel]=useState(()=>makeModel(initialSpring));
  const [ready,setReady]=useState(false),[running,setRunning]=useState(false),[reduced,setReduced]=useState(false),[status,setStatus]=useState('Initial captured model. Change settings and run to capture another response.');
  const frame=useRef<number|null>(null),epoch=useRef(0),reducedPreference=useRef(false);
  const currentModel=useRef(model),playing=useRef(false);
  const cursor=useRef<SVGLineElement>(null),readout=useRef<HTMLSpanElement>(null);
  const parameters=Object.fromEntries(fields.map(field=>[field.key,editor[field.key].trim()===''?NaN:Number(editor[field.key])])) as unknown as SpringParameters;
  const errors=validateSpring(parameters);
  const valid=errors.length===0;
  const stopFrame=()=>{epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);frame.current=null;playing.current=false;setRunning(false);};
  const showTime=(captured:ReturnType<typeof makeModel>,time:number)=>{
    const point=springResponse(captured.parameters,time);
    const x=10+980*time/captured.parameters.duration;
    cursor.current?.setAttribute('x1',String(x));cursor.current?.setAttribute('x2',String(x));
    if(readout.current)readout.current.textContent=`t=${point.time.toFixed(3)} s; x=${point.position.toFixed(4)}; velocity=${point.velocity.toFixed(4)} model units/s`;
  };
  useEffect(()=>{
    const query=matchMedia('(prefers-reduced-motion: reduce)');
    const update=()=>{
      reducedPreference.current=query.matches;setReduced(query.matches);
      if(query.matches&&playing.current){epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);frame.current=null;playing.current=false;setRunning(false);
        const captured=currentModel.current,time=captured.parameters.duration,point=springResponse(captured.parameters,time),x=990;
        cursor.current?.setAttribute('x1',String(x));cursor.current?.setAttribute('x2',String(x));
        if(readout.current)readout.current.textContent=`t=${time.toFixed(3)} s; x=${point.position.toFixed(4)}; velocity=${point.velocity.toFixed(4)} model units/s`;
        setStatus('Reduced motion is now enabled. Showing the captured endpoint without moving the cursor.');
      }
    };
    update();setReady(true);query.addEventListener('change',update);
    return()=>{epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);query.removeEventListener('change',update);};
  },[]);
  const run=()=>{
    if(!valid)return;stopFrame();const captured=makeModel(parameters);currentModel.current=captured;setModel(captured);showTime(captured,0);
    if(reducedPreference.current){showTime(captured,captured.parameters.duration);setStatus('Reduced motion: captured the response and displayed its endpoint without cursor animation.');return;}
    const token=epoch.current,started=performance.now();playing.current=true;setRunning(true);setStatus('Captured response playback started. The time horizon is not a settling guarantee.');
    const advance=(now:number)=>{
      if(token!==epoch.current)return;
      const time=Math.min(captured.parameters.duration,Math.max(0,(now-started)/1000));showTime(captured,time);
      if(time>=captured.parameters.duration){frame.current=null;playing.current=false;setRunning(false);setStatus('Captured response reached its preview horizon. Inspect the endpoint and residual velocity; playback completion is not convergence.');}
      else frame.current=requestAnimationFrame(advance);
    };frame.current=requestAnimationFrame(advance);
  };
  const y=(position:number)=>110-100*(position-model.minimum)/(model.maximum-model.minimum);
  const path=model.samples.map((point,index)=>`${index?'L':'M'}${(10+980*point.time/model.parameters.duration).toFixed(3)},${y(point.position).toFixed(3)}`).join(' ');
  const endpoint=model.samples[model.samples.length-1];
  const rows=[0,.25,.5,.75,1].map(fraction=>springResponse(model.parameters,model.parameters.duration*fraction));
  return <div className="space-y-4" data-spring-ready={ready}>
    <p>This dimensionless unit-step model moves from x=0 toward x=1. Its parameters are model units, not Android spring tokens or a physical device calibration.</p>
    <fieldset className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"><legend className="font-semibold mb-3">Draft model parameters</legend>{fields.map(field=><label key={field.key} htmlFor={`${id}-${field.key}`}>{field.label}<input id={`${id}-${field.key}`} data-spring-field={field.key} type="number" min={field.min} max={field.max} step="any" className="mt-input mt-2 w-full" value={editor[field.key]} disabled={!ready} aria-invalid={!Number.isFinite(parameters[field.key])||parameters[field.key]<field.min||parameters[field.key]>field.max} aria-describedby={`${id}-help ${id}-errors`} onChange={event=>setEditor(current=>({...current,[field.key]:event.target.value}))} /></label>)}</fieldset>
    <p id={`${id}-help`}>Mass 0.1–10; stiffness 1–1000; damping 0–100; initial velocity −5–5; preview 0.1–8 seconds. Draft changes do not rewrite the captured curve. An empty field is invalid.</p>
    <p id={`${id}-errors`} data-spring-errors>{errors.length?errors.join(' '):'Draft parameters are within the model bounds.'}</p>
    <div className="spring-response-actions flex flex-wrap gap-3"><Button type="button" data-spring-run disabled={!ready||!valid} onClick={run}>Capture and play response</Button><Button type="button" variant="outline" data-spring-stop disabled={!ready} onClick={()=>{stopFrame();setStatus('Playback stopped. The captured response and current time remain available.');}}>Stop playback</Button><Button type="button" variant="outline" data-spring-reset disabled={!ready} onClick={()=>{stopFrame();showTime(currentModel.current,0);setStatus('Cursor reset to the captured response’s start. Draft parameters are unchanged.');}}>Reset cursor</Button></div>
    <p data-spring-preference>{reduced?'Reduced motion enabled: no animated cursor.':'Reduced motion not requested: cursor playback available.'}</p>
    <div className="border border-[var(--color-muted-foreground)] rounded-md p-3" data-spring-plot>
      <svg viewBox="0 0 1000 120" preserveAspectRatio="none" className="w-full h-44" aria-hidden="true" focusable="false">
        <line x1="10" x2="990" y1={y(1)} y2={y(1)} stroke="var(--color-muted-foreground)" strokeDasharray="5 5" vectorEffect="non-scaling-stroke" />
        <path d={path} fill="none" stroke="var(--color-foreground)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
        <line ref={cursor} x1="10" x2="10" y1="0" y2="120" stroke="var(--color-foreground)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
      </svg>
      <p>Response over {model.parameters.duration} {model.parameters.duration===1?'second':'seconds'}; dashed line is target x=1. The vertical scale adapts to the captured samples. Equivalent values are below.</p>
    </div>
    <p data-spring-live aria-live="off"><span ref={readout}>t=0.000 s; x=0.0000; velocity=0.0000 model units/s</span></p>
    <dl className="grid gap-3 sm:grid-cols-2" data-spring-captured>{fields.map(field=><div key={field.key}><dt>Captured {field.label.toLowerCase()}</dt><dd data-spring-parameter={field.key}>{model.parameters[field.key]}</dd></div>)}<div><dt>Captured regime</dt><dd data-spring-regime>{model.characteristics.regime}</dd></div><div><dt>Damping ratio</dt><dd>{model.characteristics.dampingRatio.toFixed(4)}</dd></div><div><dt>Natural angular frequency</dt><dd>{model.characteristics.naturalFrequency.toFixed(4)} rad/s</dd></div><div><dt>Endpoint and velocity</dt><dd data-spring-endpoint>x={endpoint.position.toFixed(4)}; v={endpoint.velocity.toFixed(4)}</dd></div></dl>
    <p>{model.parameters.damping===0?'Zero damping retains oscillation in this ideal model. A bounded playback does not make it settle.':'Positive damping approaches the target, but this chosen horizon may end while displacement or velocity remains.'}</p>
    <div className="doc-table" role="region" tabIndex={0} aria-label="Captured spring response samples"><table><thead><tr><th scope="col">Seconds</th><th scope="col">Position</th><th scope="col">Velocity</th></tr></thead><tbody>{rows.map(point=><tr key={point.time}><th scope="row">{point.time.toFixed(3)}</th><td>{point.position.toFixed(6)}</td><td>{point.velocity.toFixed(6)}</td></tr>)}</tbody></table></div>
    <details data-spring-full><summary className="min-h-11 cursor-pointer">Full captured parameters and {model.samples.length} samples</summary><CodeBlock language="json" code={JSON.stringify({parameters:model.parameters,samples:model.samples},null,2)} /></details>
    <p role="status" data-spring-status>{status}</p>
    <p className="text-sm">{running?'Playback active.':'Playback inactive.'} The curve is sampled at up to 60 intervals per second; the cursor evaluates the captured analytic response at elapsed browser time. This plot does not animate a production component, preserve retargeted velocity or measure frame-rate performance.</p>
  </div>;
}

Scoped control reflow

The explorer’s action labels wrap and grow at enlarged text sizes. Their target height remains at least 44 CSS px without forcing the page wider than its viewport.

css
.spring-response-actions .mt-btn {
  min-block-size:44px;
  block-size:auto;
  max-inline-size:100%;
  white-space:normal;
  overflow-wrap:anywhere;
}

Verification and failure cases

  • Check analytic reference cases: initial conditions, critical response, known real roots and undamped oscillation.
  • Exercise every regime, velocity sign, extreme valid inputs, blank fields and out-of-bound values without nonfinite output.
  • Change draft parameters and verify the old capture remains unchanged until an explicit valid run.
  • Replay rapidly, stop and reset; verify the last operation owns the cursor/readout and no stale callback updates it.
  • Enable reduced motion before and during playback; verify stationary cursor behavior with complete data available.
  • Inspect both themes, 320px reflow, keyboard controls, expanded full JSON and nonannouncing frame updates.
  • Do not claim native preset parity, production component integration, continuous retargeting, physical calibration or device performance from this numerical plot.