{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"thrown-marquee","title":"Thrown Marquee","description":"A marquee you can grab and throw. Velocity decays exponentially toward the idle drift rather than toward zero, so a hard throw drops fast then creeps and the band returns to its resting speed with no special case.","dependencies":[],"files":[{"path":"src/components/ThrownMarquee/index.js","content":"export { default } from './ThrownMarquee.jsx'\n","type":"registry:component","target":"components/thrown-marquee/index.js"},{"path":"src/components/ThrownMarquee/ThrownMarquee.jsx","content":"'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport './ThrownMarquee.css'\n\n/* =========================================================\n   ThrownMarquee\n   A marquee you can grab and throw. It takes your momentum,\n   bleeds it off to friction, and settles back to its own\n   idle drift.\n\n   The usual marquee is `animation: scroll linear infinite`,\n   which cannot be interrupted, cannot be thrown, and has no\n   opinion about where it is. This one integrates two terms:\n   a drive d, which is the idle drift, and a momentum p on\n   top of it, which is whatever your hand left behind.\n\n     v = d + p\n     p ← p·e^(−λ·dt)\n\n   The exponential is the point. Friction is proportional to\n   speed, so the decay is exponential rather than linear,\n   which is why a hard throw slows dramatically at first and\n   then creeps, instead of ramping down at a constant rate.\n   With the drive at speed, v − v_idle = p, so the band\n   decays toward its resting drift rather than toward zero,\n   with no separate case for \"stopped\". Position takes the\n   exact integral, x += p·(1 − e^(−λ·dt))/λ, not p·dt, so a\n   throw travels the same p₀/λ on a 60 Hz and a 120 Hz screen.\n\n   The drive is a motor, not a constant. Grabbing the band\n   declutches it: whatever holds the band (a pointer, a\n   wheel) stops it dead, drive and all, and when it is let\n   go the drive re-engages from rest as a critically damped\n   response. So the band starts gently (zero initial\n   acceleration) and arrives at its drift with no overshoot,\n   rather than jerking to full speed. A pause (`paused`,\n   hover with `pauseOnHover`, or keyboard focus) brakes the\n   drive quickly, and only the drive: a throw released under\n   a hovering pointer still glides out on its own friction,\n   and a paused band can still be dragged, scrubbed and\n   stepped. Something that moves by itself owes its reader a\n   way to stop it, and a finger cannot hover. So under\n   `pauseOnHover` a touch that ends on the band counts as a\n   pointer that stayed: the drift holds until a pointer comes\n   down somewhere else, the way a tapped link keeps its\n   :hover. A touch the page took for a scroll does not count.\n   `paused` is where your own pause control plugs in.\n\n   A throw is only as good as the velocity measured for it.\n   The last pointer event is a poor estimate: it is noisy,\n   and if the hand stopped before letting go it still reports\n   the speed from before it stopped. A straight line through\n   the last 80 ms is steady but late: its slope is the average\n   over the window, so a hand easing off to set an item down\n   hands over the speed it had 40 ms earlier, and the band\n   slides on past where it was put. So the release velocity is\n   the slope, at the last sample, of a least-squares parabola\n   through the last 80 ms, coalesced events included, which\n   follows a hand that is speeding up or easing off. The line\n   keeps it honest: the parabola must agree with it in sign\n   and go at most half as fast again, or it is extrapolating\n   noise. A hand that has not moved for 40 ms hands over\n   nothing.\n\n   The handover is timed as well as measured. A hand lets go\n   between two frames, so the first frame of the throw owes\n   only the time since the release. Give it a whole frame and\n   it shows the hand's last move and a frame of flight in one\n   picture: the band lurches at up to twice its speed at the\n   moment you let go. Two smaller debts are settled with it,\n   so the pictures either side of the release are evenly\n   spaced. A drag is shown a few milliseconds late, each frame\n   painting the newest sample it has, and the throw keeps that\n   same lateness. And a lifted finger reports that it lifted,\n   not where it had got to, so the throw is owed the travel\n   since its last sample, up to one sample's worth.\n\n   Catching is the reverse, and it is a press, not a click. A\n   press that stops a band still flying, 150 px/s or more above\n   its drift, is the catch: the click it would have made on\n   whatever link was passing under it is swallowed, the same as\n   the click at the end of a drag.\n\n   A horizontal wheel or trackpad scrubs the band 1:1, through\n   a 40 ms glide that turns a mouse's 100 px notch into a slide\n   instead of a jump. It holds the band while it moves, and the\n   platform's own momentum carries it, so there is no second\n   throw on top. A wheel gesture keeps one axis, and it is\n   chosen over the gesture's first 8 px, not by its first\n   event: a trackpad opens with a pixel or two that can point\n   either way, and a gesture locked on that would let a page\n   scroll seize the band, or a scrub scroll the page. Once\n   chosen it holds, so a page scroll that strays sideways never\n   catches the band and a scrub that strays never scrolls the\n   page.\n\n   Keyboard focus pauses the drift, and each arrow key brings\n   the next item in that direction to rest just inside the\n   edge. The step is a critically damped seek that starts at\n   whatever speed the band has, so a second press mid-step, or\n   a press the other way, bends the motion instead of\n   restarting it, and a step asked of a band in flight lands on\n   the nearest item it can reach without braking harder than a\n   step does. A step of 100 px lands in a quarter of a second.\n   A longer one is given longer, as distance^0.35 (1.3× the\n   time for twice the distance), because the same quarter\n   second would send a wide item past at a speed nobody can\n   read. It is aimed at a whole device pixel from the start, so\n   nothing is left to correct once it lands. A held key takes\n   its next step as the last one lands, five a second at most,\n   not the thirty a keyboard repeats at.\n\n   Content is repeated as many times as it takes to cover the\n   box plus a copy either side, so the seam is always off\n   screen and there is no jump to hide. Only the second copy is\n   real; the rest are hidden from assistive tech and from Tab.\n   It starts exactly where the markup alone drew the first, so\n   mounting, or hydrating a server render, moves nothing.\n   A wrap moves the offset by one whole pitch, which leaves the\n   picture unchanged but carries the real copy, and any focus\n   ring on it, a pitch away. So the band wraps only when a copy\n   would stop covering an edge, never on a fixed modulo, and\n   that range is at least two copies wide. The wrap has\n   hysteresis: after one, the band must travel a whole copy\n   before the next, so the real copy cannot flicker back and\n   forth, and a band that settles anywhere inside the range\n   stays put. Tab to a link inside and the band works out where\n   it would have stopped anyway, the drive braking and any throw\n   still gliding, and adds only the short glide that brings the\n   real link into view there, as little as it can, clear of\n   either end of the range. Nothing stops dead for it.\n\n   Where the line runs right to left the band is its own\n   mirror image: the copies stack leftward and the drift still\n   runs toward the start of the line, so the text passes in\n   reading order. The hand, the wheel and the arrow keys stay\n   physical.\n\n   In motion the offset keeps its fraction, which is what makes\n   a 40 px/s drift smooth instead of a stutter of whole pixels.\n   Left to rest, the band takes the last half pixel or less\n   onto a whole device pixel, so type that has stopped is not\n   left resampled between two.\n   ========================================================= */\n\nconst clamp = (n, lo, hi) => (n < lo ? lo : n > hi ? hi : n)\nconst finite = (n, fallback) => (Number.isFinite(n) ? n : fallback)\n\n// The drive's responses, as critically damped natural frequencies in rad/s.\n// Settling to 95% takes about 4.74/ω: ~0.95 s to re-engage, ~0.4 s to pause.\nconst ENGAGE = 5\nconst BRAKE = 12\nconst STEP = 20 // rad/s, an arrow-key step of NEAR or less, also critically damped: 95% in 0.24 s\nconst NEAR = 100 // px, the step STEP is tuned for: a longer one takes longer\nconst REPEAT = 200 // ms, the least time between the steps of a held arrow key\nconst LANDED = 0.08 // of a step still to go when a held key may take the next\nconst GLIDE = 0.04 // s, time constant a wheel delta is spent over\nconst WHEEL_HOLD = 90 // ms without a wheel event before the band is let go\nconst AXIS = 8 // px a wheel gesture travels before its axis is settled\nconst FIT = 80 // ms of pointer history the release velocity is fitted to\nconst STOPPED = 40 // ms without movement that means the hand had stopped\nconst SLOP = 4 // px a press may wander and still be a click\nconst CATCH = 150 // px/s of throw above which a still press is a catch, not a click\nconst INSET = 12 // px inside the edge a revealed or stepped-to item comes to rest\nconst MIN_STEP = 24 // px, the least an arrow key moves the band, so every press shows\nconst MAX_COPIES = 64\n\n// An arrow-key step's natural frequency, rad/s. Its time grows as\n// distance^0.35 (1.3× at twice NEAR, 1.6× at four times) and stops at 1.8×.\nconst stepRate = (dist) => STEP * clamp((NEAR / Math.max(1, Math.abs(dist))) ** 0.35, 1 / 1.8, 1)\n\nconst FOCUSABLE = ['a[href]', 'button', 'input', 'select', 'textarea', 'summary', 'iframe', '[tabindex]', '[contenteditable]']\n  .map((s) => `.thrown-marquee__copy[aria-hidden] ${s}`)\n  .join(',')\n\nexport default function ThrownMarquee({\n  speed = 40, // idle drift, px/s, toward the start of the line — negative runs the other way\n  friction = 1.6, // λ, per second — how fast a throw bleeds back to the drift\n  throwScale = 1, // how much of a flick's velocity is taken\n  maxSpeed = 4000, // px/s, the fastest a throw can go\n  gap = 48, // px between items, and between copies\n  paused = false, // hold the drift at rest: the hook for your own pause control\n  pauseOnHover = false, // brake the drift to rest under a hovering pointer\n  wheel = true, // a horizontal wheel or trackpad scrubs the band\n  children,\n  className = '',\n  style,\n  ...rest\n}) {\n  const rootRef = useRef(null)\n  const trackRef = useRef(null)\n  const [copies, setCopies] = useState(3)\n\n  const live = useRef({})\n  const wake = useRef(null)\n  // Synced after every render and before the effects below, so the loop reads\n  // current props without being torn down and restarted. The loop may have\n  // stopped at rest, so a new prop (a speed, say) has to wake it.\n  useEffect(() => {\n    live.current = { speed, friction, throwScale, maxSpeed, paused, pauseOnHover, wheel }\n    wake.current?.()\n  })\n\n  // The copies are decoration: aria-hidden takes them out of the reading\n  // order, and this takes their links and buttons out of the tab order.\n  useEffect(() => {\n    trackRef.current?.querySelectorAll(FOCUSABLE).forEach((el) => el.setAttribute('tabindex', '-1'))\n  })\n\n  useEffect(() => {\n    const root = rootRef.current\n    const track = trackRef.current\n    if (!root || !track) return\n\n    const motion = window.matchMedia('(prefers-reduced-motion: reduce)')\n    let still = motion.matches\n\n    let span = 1 // pitch of one copy, including the gap after it, local px\n    let view = 0 // the box's width, local px\n    let count = 3 // copies the box calls for: enough to cover it, and one either side\n    let measured = false\n    let scale = 1 // screen px per local px, in case an ancestor is transformed\n    let flip = 1 // −1 where the line runs right to left, and the band is its own mirror image\n    let pos = 0 // the track's offset, local px, kept in [low(), 0] by wrap()\n    let x = 0 // distance travelled, local px, never wrapped: what a throw is fitted to\n    let p = 0 // momentum above the drive, px/s\n    let d = 0 // the drive's speed, px/s\n    let dd = 0 // and its rate of change, px/s²\n    let pend = 0 // displacement still to glide off (wheel deltas)\n    let sr = 0 // a seek's distance still to go (a key step, a reveal), px\n    let sv = 0 // and its speed, px/s\n    let sw = BRAKE // and its natural frequency, rad/s\n    let raf = 0\n    let last = 0\n    let onScreen = true\n    let painted = ''\n    let hovering = false // a mouse or a pen is over the band\n    let touched = false // a touch ended on the band, and no pointer has come down elsewhere since\n    let keyed = false\n    let stepAt = 0 // when the last arrow-key step was taken\n    let stepLen = 0 // and how far it had to go, px\n\n    let hand = null // pointerId of the pointer holding the band\n    let handX = 0\n    let travel = 0\n    let captured = false\n    let dragging = false // the press has wandered past SLOP: it is a drag, not a click\n    let caught = false // the press stopped a throw\n    let samples = [] // [timeStamp, x]\n    let fresh = false // a sample no frame has shown yet\n    let late = 0 // ms from a sample to the frame that shows it\n    let wheelUntil = 0 // while a sideways wheel gesture holds the band\n    let wheelAxis = 0 // the current wheel gesture's axis, once settled: 1 is ours, −1 the page's\n    let wheelX = 0 // until then, how far the gesture has gone sideways, px\n    let wheelY = 0 // and up or down\n    let wheelLast = 0\n    let eatClickUntil = 0\n\n    const readBox = () => {\n      const w = root.offsetWidth\n      scale = w ? root.getBoundingClientRect().width / w || 1 : 1\n      flip = getComputedStyle(root).direction === 'rtl' ? -1 : 1\n    }\n\n    // How far along the line one box starts after another, local px: left\n    // edge to left edge, or right to right where the line runs right to left.\n    const along = (a, from) => (flip > 0 ? a.left - from.left : from.right - a.right) / scale\n\n    // The lowest offset at which the last copy still reaches the trailing\n    // edge. Offset 0 is where the first copy just reaches the leading one.\n    // It counts the copies the box calls for, not the ones React has rendered\n    // so far, so it is at least two pitches below 0 from the first frame and\n    // the band's first offset, one pitch down, is not sitting on a boundary\n    // that a rounding could tip it over.\n    const low = () => Math.min(-span, view - count * span)\n\n    // Wrap by whole copies, and only on leaving [low, 0], never on a fixed\n    // modulo: after a wrap the band must travel a whole copy before the next,\n    // so the real copy (and a focus ring on it) cannot flicker between two.\n    const fold = (at) => {\n      if (!Number.isFinite(at)) at = -span\n      if (at > 0) at -= Math.ceil(at / span) * span\n      const lo = low()\n      if (at < lo) at += Math.ceil((lo - at) / span) * span\n      return at\n    }\n    const wrap = () => {\n      pos = fold(pos)\n    }\n\n    // Every motion comes through here: x keeps the whole distance for the\n    // velocity fit, pos the picture.\n    const move = (dist) => {\n      x += dist\n      pos -= dist\n      wrap()\n    }\n\n    const measure = () => {\n      const a = track.children[0]\n      const b = track.children[1]\n      if (!a || !b || !root.offsetWidth) return\n      readBox()\n      view = root.clientWidth\n      // The distance between two copies' left edges is the exact pitch,\n      // sub-pixel and gap included, which scrollWidth / n rounds away.\n      const next = along(b.getBoundingClientRect(), a.getBoundingClientRect())\n      if (next > 0.5 && Math.abs(next - span) > 0.01) {\n        // Keep the phase, so a new gap or new content does not jump a whole\n        // copy. At first the real copy takes the place the first one was drawn\n        // in before there was a transform: the same picture, so nothing jumps.\n        pos = measured ? (pos / span) * next : -next\n        span = next\n        measured = true\n      }\n      count = a.offsetWidth < 1 ? 3 : clamp(2 + Math.ceil(view / span), 3, MAX_COPIES)\n      wrap()\n      setCopies(count)\n    }\n\n    const paint = () => {\n      const t = `translate3d(${(flip * pos).toFixed(2)}px,0,0)`\n      if (t !== painted) {\n        track.style.transform = t\n        painted = t\n      }\n    }\n\n    const braked = () => live.current.paused || (live.current.pauseOnHover && (hovering || touched)) || keyed\n\n    // Whatever takes hold of the band stops it, drive included.\n    const hold = () => {\n      p = 0\n      d = 0\n      dd = 0\n      sr = 0\n      sv = 0\n    }\n\n    // The band's velocity, every term of it. A glide moves at what is left\n    // of it over its time constant.\n    const velocity = () => d + p + sv + pend / GLIDE\n\n    const frame = (now) => {\n      // `last` is a frame's time, or a release's (see release), which the\n      // next frame can fall a hair short of: so the step is held at both ends.\n      const dt = last ? clamp((now - last) / 1000, 0, 1 / 30) : 1 / 60\n      last = now\n      const idle = finite(live.current.speed, 0)\n      const lam = Math.max(0, finite(live.current.friction, 0))\n\n      if (wheelUntil && now > wheelUntil) wheelUntil = 0\n\n      // How late this frame shows the hand: the throw is handed the same.\n      if (fresh && hand !== null) late = now - samples[samples.length - 1][0]\n      fresh = false\n\n      // A glide spends its displacement exponentially: it answers at once and\n      // lands softly, and a second notch just adds to what is left.\n      if (pend) {\n        let step = still ? pend : pend * (1 - Math.exp(-dt / GLIDE))\n        if (Math.abs(pend - step) < 0.3) step = pend\n        move(step)\n        pend -= step\n      }\n\n      if (hand === null && !wheelUntil) {\n        // A seek: critically damped onto its rest, from the speed it took\n        // over, integrated exactly. r = (r₀ + (ω·r₀ − v₀)·t)·e^(−ωt).\n        if (sr || sv) {\n          const k = Math.exp(-sw * dt)\n          const b = sw * sr - sv\n          const r = (sr + b * dt) * k\n          sv = (sv + sw * b * dt) * k\n          move(sr - r)\n          sr = r\n          if (Math.abs(sr) < 0.1 && Math.abs(sv) < 2) {\n            move(sr)\n            sr = 0\n            sv = 0\n          }\n        }\n\n        const stopped = still || braked()\n        const target = stopped ? 0 : idle\n        if (still) {\n          d = 0\n          dd = 0\n        } else if (d !== target || dd) {\n          // Critically damped drive, integrated exactly: e is the error, dd its rate.\n          const w = stopped ? BRAKE : ENGAGE\n          const e = d - target\n          const k = Math.exp(-w * dt)\n          const q = (dd + w * e) * dt\n          const d0 = d\n          d = target + (e + q) * k\n          dd = (dd - w * q) * k\n          if (Math.abs(d - target) < 0.5 && Math.abs(dd) < 5) {\n            d = target\n            dd = 0\n          }\n          move(((d0 + d) / 2) * dt)\n        } else if (d) {\n          move(d * dt)\n        }\n\n        if (p) {\n          const k = Math.exp(-lam * dt)\n          move(lam > 0 ? (p * (1 - k)) / lam : p * dt)\n          p *= k\n          // Below 2 px/s (a thirtieth of a pixel a frame) the throw is over:\n          // stop it there rather than let a sub-pixel tail keep the loop alive.\n          if (Math.abs(p) < 2) p = 0\n        }\n      }\n\n      const busy = pend || wheelUntil || (hand === null && (p || d || dd || sr || sv))\n      // In motion the offset keeps its fraction. Left to rest, the band takes\n      // the last half pixel or less onto a whole device pixel, so its type is\n      // not left resampled between two. A hand holds it exactly where it is.\n      if (!busy && hand === null) {\n        const dpr = window.devicePixelRatio || 1\n        move(pos - Math.round(pos * dpr) / dpr)\n      }\n      paint()\n      if (!busy) {\n        raf = 0\n        last = 0\n        return\n      }\n      raf = requestAnimationFrame(frame)\n    }\n\n    const kick = () => {\n      if (raf || !onScreen) return\n      last = 0\n      raf = requestAnimationFrame(frame)\n    }\n    wake.current = kick\n\n    /* ---------- the hand ---------- */\n\n    const follow = (ev) => {\n      const dx = ev.clientX - handX\n      handX = ev.clientX\n      if (!dx) return\n      travel += Math.abs(dx)\n      move((-flip * dx) / scale)\n      samples.push([ev.timeStamp, x])\n      fresh = true\n    }\n\n    // The hand's velocity at the last sample, px/s: the slope there of a\n    // least-squares parabola through the last FIT ms, held to the straight\n    // line's sign and to half as fast again. A hand that had stopped before\n    // letting go hands over nothing.\n    const releaseVelocity = (t) => {\n      const n = samples.length\n      if (n < 2 || t - samples[n - 1][0] > STOPPED) return 0\n      const [t0, x0] = samples[n - 1]\n      const pts = samples.filter(([st]) => st >= t0 - FIT)\n      if (pts.length < 2 || t0 - pts[0][0] < 4) return 0\n      // Moments in u = time before the last sample (ms), y = travel since it.\n      let s0 = 0\n      let s1 = 0\n      let s2 = 0\n      let s3 = 0\n      let s4 = 0\n      let y0 = 0\n      let y1 = 0\n      let y2 = 0\n      for (const [st, sx] of pts) {\n        const u = st - t0\n        const y = sx - x0\n        s0 += 1\n        s1 += u\n        s2 += u * u\n        s3 += u * u * u\n        s4 += u * u * u * u\n        y0 += y\n        y1 += y * u\n        y2 += y * u * u\n      }\n      const flat = s0 * s2 - s1 * s1\n      if (!(flat > 0)) return 0\n      const line = (s0 * y1 - s1 * y0) / flat\n      // y = a + b·u + c·u², solved for b by Cramer's rule.\n      const det = s0 * (s2 * s4 - s3 * s3) - s1 * (s1 * s4 - s3 * s2) + s2 * (s1 * s3 - s2 * s2)\n      if (pts.length < 3 || !(det > 1e-6 * s0 * s2 * s4)) return line * 1000\n      const b = (s0 * (y1 * s4 - s3 * y2) - y0 * (s1 * s4 - s3 * s2) + s2 * (s1 * y2 - y1 * s2)) / det\n      if (!(b * line > 0)) return 0\n      return Math.sign(b) * Math.min(Math.abs(b), 1.5 * Math.abs(line)) * 1000\n    }\n\n    const onDown = (e) => {\n      // A second pointer cannot take the band from the first. The same one\n      // pressing again means its release never arrived, so it starts over.\n      if ((hand !== null && e.pointerId !== hand) || (e.pointerType === 'mouse' && e.button !== 0)) return\n      hand = e.pointerId\n      handX = e.clientX\n      travel = 0\n      captured = false\n      dragging = false\n      caught = Math.abs(p + sv + pend / GLIDE) > CATCH\n      eatClickUntil = 0\n      readBox()\n      hold()\n      pend = 0\n      samples = [[e.timeStamp, x]]\n      fresh = false\n      root.classList.add('is-held')\n    }\n\n    const onMove = (e) => {\n      if (e.pointerId !== hand) return\n      // No button down: the release was missed (it happened over another\n      // window, say). Let go, not follow a pointer that is only hovering.\n      if (!e.buttons) {\n        release(e, false)\n        return\n      }\n      const list = e.getCoalescedEvents?.()\n      if (list && list.length) for (const ev of list) follow(ev)\n      else follow(e)\n      const cut = e.timeStamp - 2 * FIT\n      while (samples.length > 2 && samples[0][0] < cut) samples.shift()\n      // Capture only once it is a drag, so a plain click still reaches a link.\n      // Until then a link under the pointer keeps its own cursor; from here on\n      // the hand is dragging, whatever it is over.\n      if (!dragging && travel > SLOP) {\n        dragging = true\n        captured = true\n        root.classList.add('is-dragging')\n        try {\n          root.setPointerCapture(e.pointerId)\n        } catch {\n          // the pointer is already gone; pointerup will still arrive on window\n        }\n      }\n      kick()\n    }\n\n    const release = (e, thrown) => {\n      if (e.pointerId !== hand) return\n      if (thrown) follow(e)\n      const { throwScale, maxSpeed } = live.current\n      const cap = Math.abs(finite(maxSpeed, 4000))\n      const v = thrown && !still ? releaseVelocity(e.timeStamp) * finite(throwScale, 1) : 0\n      p = clamp(v, -cap, cap)\n      hand = null\n      // A finger lifted off the band stands for a pointer left hovering over\n      // it. One the browser took away (a page scroll, a pinch) does not, and\n      // neither does one that was dragged off the band before it lifted.\n      if (thrown && e.pointerType === 'touch') {\n        const r = root.getBoundingClientRect()\n        touched = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom\n      }\n      // A drag is not a click, and neither is the press that caught a throw.\n      if (travel > SLOP || caught) eatClickUntil = e.timeStamp + 300\n      if (captured) {\n        try {\n          root.releasePointerCapture(e.pointerId)\n        } catch {\n          // already released\n        }\n      }\n      captured = false\n      dragging = false\n      root.classList.remove('is-held', 'is-dragging')\n      kick()\n      if (p) {\n        // The throw starts when the hand let go, shown as late as the drag\n        // was, so its first frame steps only the time since then and not a\n        // whole one. A lifted finger says that it lifted, not where it had\n        // got to, so the throw is also owed the travel since the last sample,\n        // up to one sample's worth: `every` is the time between samples.\n        const n = samples.length - 1\n        const t1 = samples[n][0]\n        const k = samples.findIndex(([st]) => st >= t1 - FIT)\n        const every = n > k ? (t1 - samples[k][0]) / (n - k) : 0\n        last = Math.max(t1, e.timeStamp - every) + late\n      }\n    }\n\n    const onUp = (e) => release(e, true)\n    const onCancel = (e) => release(e, false)\n    // A touch is captured by whatever it landed on, so when the band takes\n    // the capture that element loses it, and its lostpointercapture bubbles\n    // up through here. Only the band losing its own capture ends the hold.\n    const onLost = (e) => {\n      if (e.target === root) release(e, false)\n    }\n\n    // A drag that ends over a link must not also follow it.\n    const onClick = (e) => {\n      if (e.timeStamp < eatClickUntil) {\n        e.preventDefault()\n        e.stopPropagation()\n      }\n      eatClickUntil = 0\n    }\n    const onDragStart = (e) => e.preventDefault()\n\n    // A touch enters as it lands and leaves as it lifts, which says nothing\n    // about hovering: `touched` answers for it. A mouse or a pen is the real\n    // thing, and takes over from a touch when it arrives.\n    const onEnter = (e) => {\n      if (e.pointerType === 'touch') return\n      hovering = true\n      touched = false\n      kick()\n    }\n    const onLeave = (e) => {\n      if (e.pointerType === 'touch') return\n      hovering = false\n      kick()\n    }\n    // A touch's hover ends when a pointer comes down anywhere off the band.\n    const onElsewhere = (e) => {\n      if (!touched || (e.target instanceof Node && root.contains(e.target))) return\n      touched = false\n      kick()\n    }\n\n    /* ---------- wheel ---------- */\n\n    const onWheel = (e) => {\n      if (!live.current.wheel || e.ctrlKey || hand !== null) return\n      let dx = e.deltaX\n      let dy = e.deltaY\n      if (e.shiftKey && !dx) {\n        dx = dy\n        dy = 0\n      }\n      const box = view || span\n      const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? box : 1\n      // One gesture, one axis: mostly vertical is the page's scroll, mostly\n      // sideways ours, and once settled it holds until the wheel has been\n      // quiet for WHEEL_HOLD, platform momentum included. A per-event test\n      // would let a stray sideways event in a page scroll stop the drift, and\n      // a stray vertical one in a scrub scroll the page. It is settled over\n      // the gesture's first AXIS px, not by its first event, which on a\n      // trackpad is a pixel or two that can point either way. Until then each\n      // event goes wherever the travel so far says, so a scrub still answers\n      // on its first event.\n      if (e.timeStamp - wheelLast > WHEEL_HOLD) {\n        wheelAxis = 0\n        wheelX = 0\n        wheelY = 0\n      }\n      wheelLast = e.timeStamp\n      let ours = wheelAxis > 0\n      if (!wheelAxis) {\n        wheelX += Math.abs(dx) * unit\n        wheelY += Math.abs(dy) * unit\n        ours = wheelX > wheelY\n        if (wheelX + wheelY >= AXIS) wheelAxis = ours ? 1 : -1\n      }\n      if (!ours) return\n      e.preventDefault()\n      pend = clamp(pend + flip * dx * unit, -box, box)\n      // Only a gesture settled on the band takes hold of it. Before that the\n      // band is nudged and no more, so the stray first pixel of a page scroll\n      // does not stop the drift.\n      if (wheelAxis) {\n        hold()\n        wheelUntil = e.timeStamp + WHEEL_HOLD\n      }\n      kick()\n    }\n\n    /* ---------- keyboard ---------- */\n\n    // Hand the band's whole remaining motion to one critically damped seek\n    // that comes to rest `go` further on, at natural frequency w, starting at\n    // the speed the band already has. So nothing stops dead for it, and a\n    // seek asked for in the middle of another bends it instead of restarting.\n    // The rest is aimed at a whole device pixel here, wraps on the way\n    // counted, so the landing has no half pixel left to hop.\n    const seek = (go, w) => {\n      const v = velocity()\n      const dpr = window.devicePixelRatio || 1\n      const rest = fold(pos - go)\n      go += rest - Math.round(rest * dpr) / dpr\n      hold()\n      pend = 0\n      wheelUntil = 0\n      if (still) {\n        pend = go\n      } else {\n        sr = go\n        sv = v\n        sw = w\n      }\n      kick()\n    }\n\n    // How far past where the band is already headed the next item in\n    // direction dir comes to rest INSET inside the leading edge. At least\n    // `least`, so a press never lands on an item a hair away and looks\n    // ignored; at most three quarters of the box, which is also the step for\n    // text with no items to step between.\n    const stepFrom = (dir, least) => {\n      const a = track.children[0]\n      const n = track.children.length\n      const from = pos - pend - sr\n      const first = a.getBoundingClientRect()\n      let best = dir * Math.max((view || span) * 0.75, least)\n      for (const el of a.children) {\n        const s = along(el.getBoundingClientRect(), first)\n        // The same item in every copy: whichever is next along.\n        for (let k = 0; k < n; k++) {\n          const dist = from + k * span + s - INSET\n          if (dir > 0 ? dist >= least && dist < best : dist <= -least && dist > best) best = dist\n        }\n      }\n      return best\n    }\n\n    const onKey = (e) => {\n      // The keys are physical: right brings on what lies to the right.\n      const dir = flip * (e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0)\n      // Only the band itself steps: moving it under a focused link inside\n      // would carry the focus out of view. Tab reaches those, and reveals them.\n      if (!dir || e.target !== root || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || hand !== null) return\n      e.preventDefault()\n      keyed = true\n      // A held key repeats some thirty times a second, faster than a step\n      // can be read: it takes its next step as the last one lands instead.\n      if (e.repeat && (e.timeStamp - stepAt < REPEAT || Math.abs(sr) > LANDED * stepLen)) return\n      stepAt = e.timeStamp\n      readBox()\n      // The step goes on from where the band is already headed. A band in\n      // flight needs v/ω of road to stop on without braking harder than a\n      // step does, so it steps to the first item at least that far ahead.\n      const v = velocity()\n      const owed = pend + sr\n      const least = Math.max(MIN_STEP, dir * (v / STEP - owed))\n      const reach = 1.5 * (view || span)\n      const go = clamp(owed + stepFrom(dir, least), -reach, reach)\n      stepLen = Math.abs(go)\n      // The longer the step the softer the seek, but never so soft that the\n      // speed the band already has would carry it past its rest (see reveal).\n      seek(go, clamp(go * v > 0 ? v / go : 0, stepRate(go), 60))\n    }\n\n    // Tabbing to a link inside makes the browser scroll this overflow box to\n    // reveal it, which the transform knows nothing about. Undo that, and glide\n    // the band instead until the real (first-copy) element is in view.\n    const onScroll = () => {\n      if (root.scrollLeft) root.scrollLeft = 0\n      if (root.scrollTop) root.scrollTop = 0\n    }\n\n    // Bring the real element into view where the band comes to rest, moving\n    // it as little as possible, and keep that rest inside [low, 0] so no wrap\n    // can carry the element off afterwards. The band's own rest comes first:\n    // the glide still owed, the drive braking to its pause (a critically\n    // damped stop from d runs on 2d/ω + d′/ω²) and the throw's remaining p/λ.\n    // If the element is in view there, nothing changes. If not, the band's\n    // whole remaining motion becomes one seek to the new rest, stiff enough\n    // not to sail past a rest that is close ahead. No dead stop, no swing.\n    const reveal = (el) => {\n      readBox()\n      const lam = Math.max(0, finite(live.current.friction, 0))\n      if (!lam) p = 0 // a frictionless throw would never land, so focus catches it\n      const ahead = pend + (2 * d) / BRAKE + dd / (BRAKE * BRAKE) + (lam ? p / lam : 0) + sr\n      const r = root.getBoundingClientRect()\n      const b = el.getBoundingClientRect()\n      const w = b.width / scale\n      const at = along(b, r) - ahead // where it would rest\n      const want = clamp(at, INSET, Math.max(INSET, (view || span) - INSET - w))\n      const rest = pos - ahead\n      const aim = clamp(rest + want - at, low() + 1, -1)\n      if (Math.abs(rest - aim) < 0.5) return\n      const v = velocity()\n      const go = pos - aim\n      // Critically damped sails past its rest when v > ω·r, so a close rest\n      // gets a stiffer seek: at ω = v/r it is a plain exponential glide that\n      // starts at exactly the speed the band had.\n      seek(go, clamp(go * v > 0 ? v / go : 0, BRAKE, 60))\n    }\n\n    const onFocusIn = (e) => {\n      let visible = true\n      try {\n        visible = e.target.matches(':focus-visible')\n      } catch {\n        // an engine without :focus-visible: treat every focus as keyboard focus\n      }\n      onScroll()\n      // Keyboard focus pauses the drift and brings a focused item into view.\n      // A click that focuses a link does neither: the hand is already there.\n      if (visible && hand === null) {\n        keyed = true\n        if (e.target !== root) reveal(e.target)\n      }\n      kick()\n    }\n\n    const onFocusOut = (e) => {\n      if (root.contains(e.relatedTarget)) return\n      keyed = false\n      kick()\n    }\n\n    const onMotion = () => {\n      still = motion.matches\n      if (still) {\n        // Settle at once: a glide or a step under way lands now.\n        const owed = pend + sr\n        hold()\n        pend = 0\n        move(owed)\n        paint()\n      }\n      kick()\n    }\n\n    const io = new IntersectionObserver((entries) => {\n      onScreen = entries[entries.length - 1].isIntersecting\n      if (onScreen) kick()\n      else if (raf) {\n        cancelAnimationFrame(raf)\n        raf = 0\n      }\n    })\n    io.observe(root)\n\n    const ro = new ResizeObserver(() => {\n      measure()\n      paint()\n      kick()\n    })\n    ro.observe(track)\n    ro.observe(root)\n\n    measure()\n    paint()\n    kick()\n\n    root.addEventListener('pointerdown', onDown)\n    root.addEventListener('pointermove', onMove)\n    root.addEventListener('lostpointercapture', onLost)\n    window.addEventListener('pointerup', onUp, true)\n    window.addEventListener('pointercancel', onCancel, true)\n    window.addEventListener('pointerdown', onElsewhere, true)\n    root.addEventListener('click', onClick, true)\n    root.addEventListener('dragstart', onDragStart)\n    root.addEventListener('pointerenter', onEnter)\n    root.addEventListener('pointerleave', onLeave)\n    root.addEventListener('wheel', onWheel, { passive: false })\n    root.addEventListener('keydown', onKey)\n    root.addEventListener('focusin', onFocusIn)\n    root.addEventListener('focusout', onFocusOut)\n    root.addEventListener('scroll', onScroll)\n    motion.addEventListener?.('change', onMotion)\n\n    let dead = false\n    document.fonts?.ready?.then(() => {\n      if (!dead) {\n        measure()\n        paint()\n        kick()\n      }\n    })\n\n    return () => {\n      dead = true\n      wake.current = null\n      io.disconnect()\n      ro.disconnect()\n      root.removeEventListener('pointerdown', onDown)\n      root.removeEventListener('pointermove', onMove)\n      root.removeEventListener('lostpointercapture', onLost)\n      window.removeEventListener('pointerup', onUp, true)\n      window.removeEventListener('pointercancel', onCancel, true)\n      window.removeEventListener('pointerdown', onElsewhere, true)\n      root.removeEventListener('click', onClick, true)\n      root.removeEventListener('dragstart', onDragStart)\n      root.removeEventListener('pointerenter', onEnter)\n      root.removeEventListener('pointerleave', onLeave)\n      root.removeEventListener('wheel', onWheel)\n      root.removeEventListener('keydown', onKey)\n      root.removeEventListener('focusin', onFocusIn)\n      root.removeEventListener('focusout', onFocusOut)\n      root.removeEventListener('scroll', onScroll)\n      motion.removeEventListener?.('change', onMotion)\n      root.classList.remove('is-held', 'is-dragging')\n      if (raf) cancelAnimationFrame(raf)\n    }\n  }, [])\n\n  const gapPx = `${finite(gap, 0)}px`\n\n  return (\n    <div\n      ref={rootRef}\n      className={`thrown-marquee ${className}`.trim()}\n      style={style}\n      tabIndex={0}\n      role=\"group\"\n      aria-roledescription=\"marquee\"\n      {...rest}\n    >\n      <div ref={trackRef} className=\"thrown-marquee__track\" style={{ gap: gapPx }}>\n        {Array.from({ length: copies }, (_, i) => (\n          // Every copy but the second is decoration: that one already carries\n          // the content for anyone reading the page rather than looking at it.\n          <div key={i} className=\"thrown-marquee__copy\" style={{ gap: gapPx }} aria-hidden={i === 1 ? undefined : 'true'}>\n            {children}\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n","type":"registry:component","target":"components/thrown-marquee/ThrownMarquee.jsx"},{"path":"src/components/ThrownMarquee/ThrownMarquee.css","content":"/* ThrownMarquee — one transform on the track, nothing else. The band is\n   repeated enough times to cover the box plus a copy either side, and the\n   offset wraps by whole copies, so the seam never enters the visible box. */\n\n.thrown-marquee {\n  overflow: hidden;\n  cursor: grab;\n  -webkit-user-select: none;\n  user-select: none;\n  -webkit-touch-callout: none;  /* no link/image callout while a finger holds the band */\n  /* The band listens for clicks (to swallow the one a drag ends with), and a\n     touch browser flashes its tap highlight over anything that does: the\n     whole band would blink like a pressed button at every touch. */\n  -webkit-tap-highlight-color: transparent;\n  /* Horizontal drags are ours. Vertical scroll and a pinch to zoom are the\n     page's, and a touch that turns into either lets go of the band. */\n  touch-action: pan-y pinch-zoom;\n}\n\n/* The band fills its container. Without this it shrinks to its content in\n   any centring grid or flex parent, and then the copies run out on screen.\n   The zero minimum is the other half: a grid or flex item is otherwise at\n   least as wide as its content, which here is every copy end to end.\n   :where() keeps both at zero specificity, so any width you set wins. */\n:where(.thrown-marquee) {\n  width: 100%;\n  min-width: 0;\n  box-sizing: border-box;\n}\n\n/* A press closes the hand at once. Over a link the pointer stays a pointer\n   while the press could still be a click, and closes once it is a drag. */\n.thrown-marquee.is-held,\n.thrown-marquee.is-dragging * { cursor: grabbing; }\n\n/* Keyboard focus pauses the drift and the arrow keys step it, so the ring\n   says the band is listening. It stands a little off the band, clear of the\n   type moving under it. Pointer focus draws nothing. */\n.thrown-marquee:focus-visible {\n  outline: 2px solid currentColor;\n  outline-offset: 4px;\n}\n\n/* The band is only as tall as its content, so the focus ring of a link\n   inside it is cut off top and bottom, leaving two short ticks at its sides.\n   While keyboard focus is inside, the band clips sideways only. (A box that\n   clips is not a scroll container, which is why the minimum above is set\n   outright and not left to `overflow: hidden` to imply.) */\n@supports (overflow: clip) {\n  .thrown-marquee:has(:focus-visible) {\n    overflow-x: clip;\n    overflow-y: visible;\n  }\n}\n\n.thrown-marquee__track {\n  display: flex;\n  width: max-content;\n  will-change: transform;\n}\n\n.thrown-marquee__copy {\n  display: flex;\n  align-items: center;\n  flex: none;\n  /* The tap highlight is inherited: hand the default back to the content, so\n     a link inside still answers a tap the way links do. */\n  -webkit-tap-highlight-color: initial;\n}\n\n/* Reduced motion keeps the band where your hand leaves it: no drift and no\n   throw, but dragging, the wheel and the arrow keys still move it. */\n@media (prefers-reduced-motion: reduce) {\n  .thrown-marquee__track { will-change: auto; }\n}\n","type":"registry:file","target":"components/thrown-marquee/ThrownMarquee.css"},{"path":"src/components/ThrownMarquee/ThrownMarquee.d.ts","content":"import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react'\n\nexport interface ThrownMarqueeProps extends ComponentPropsWithoutRef<'div'> {\n  /** Idle drift, px/s, toward the start of the line (leftward in left-to-right text). Negative runs the other way. The drift re-engages from rest, critically damped, after the band is let go. */\n  speed?: number\n  /** Friction λ, per second. A throw's momentum decays as e^(−λt) on top of the idle drift, so it travels v₀/λ. 0 is frictionless. */\n  friction?: number\n  /** How much of a flick's measured velocity (a least-squares fit over the last 80 ms) the band takes. */\n  throwScale?: number\n  /** Fastest a throw can go, px/s. */\n  maxSpeed?: number\n  /** Space between items, and between copies, px. */\n  gap?: number\n  /** Hold the drift at rest, braking to it and re-engaging from it without a jump. The hook for your own pause control: a paused band can still be dragged, thrown, scrubbed and stepped. */\n  paused?: boolean\n  /** Brake the drift to rest while a pointer hovers the band. A throw still glides out on its own friction. A finger cannot hover, so a touch that ends on the band holds the drift until a pointer comes down somewhere else. */\n  pauseOnHover?: boolean\n  /** Let a horizontal wheel or trackpad scrub the band 1:1. A gesture keeps the axis of its first 8 px, so vertical scrolling always passes through to the page. */\n  wheel?: boolean\n  /** The items on the band, repeated end to end to fill it. */\n  children?: ReactNode\n}\n\ndeclare const ThrownMarquee: (props: ThrownMarqueeProps) => ReactElement\nexport default ThrownMarquee\n","type":"registry:lib","target":"components/thrown-marquee/ThrownMarquee.d.ts"}],"type":"registry:component"}