{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "speed-lines",
  "title": "Speed Lines",
  "description": "Parallel streaks trail an element as it flies in at its real speed, the element stretching with that speed and landing with a squash and a small bounce as the streaks die, or radial lines rush in from the frame edges and snap onto one point for a manga-style impact frame.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/speed-lines/index.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { random, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type SpeedLinesMode = \"trail\" | \"focus\";\n\nexport interface SpeedLinesPoint {\n  x: number;\n  y: number;\n}\n\nexport interface SpeedLinesProps {\n  mode?: SpeedLinesMode;\n  from?: SpeedLinesPoint;\n  to?: SpeedLinesPoint;\n  x?: number;\n  y?: number;\n  count?: number;\n  weight?: number;\n  spread?: number;\n  duration?: number;\n  bounce?: number;\n  squash?: number;\n  color?: string;\n  seed?: number;\n  delay?: number;\n  speed?: number;\n  children?: ReactNode;\n  className?: string;\n}\n\ntype Paint = \"color\" | \"children\" | \"className\";\ntype Unset = \"to\" | \"children\" | \"className\";\n\nexport interface SpeedLinesOptions extends Omit<SpeedLinesProps, Paint> {\n  width?: number;\n  height?: number;\n  fps?: number;\n}\n\nexport interface SpeedLinesSegment {\n  index: number;\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  length: number;\n}\n\nexport interface SpeedLinesStreak extends SpeedLinesSegment {\n  offset: number;\n  reach: number;\n}\n\nexport interface SpeedLinesRay extends SpeedLinesSegment {\n  angle: number;\n  rest: number;\n  inner: number;\n  outer: number;\n}\n\nexport interface SpeedLinesRayTiming {\n  rest: number;\n  outer: number;\n  start?: number;\n  release?: number;\n}\n\nexport const speedLinesDefaults: Required<Omit<SpeedLinesProps, Unset>> = {\n  mode: \"trail\",\n  from: { x: -0.25, y: 0.5 },\n  x: 0.5,\n  y: 0.5,\n  count: 12,\n  weight: 2,\n  spread: 96,\n  duration: 14,\n  bounce: 0.5,\n  squash: 0.5,\n  color: \"#ffffff\",\n  seed: 1,\n  delay: 0,\n  speed: 1,\n};\n\nconst REFERENCE = 720;\nconst LANDING = 0.3;\nconst CARRY = 0.3;\nconst ECHO = 0.2;\nconst SOFTEN = 1.2;\nconst SETTLE = 6;\nconst STRETCH = Math.log(1.3);\nconst SQUASH = Math.log(1.25);\nconst STRETCH_GAIN = 0.007;\nconst SQUASH_GAIN = 0.018;\nconst PROBE = 1e-3;\nconst CENTER = \"translate(-50%, -50%)\";\nconst HOLD = 24;\nconst SPAWN_START = 0.04;\nconst SPAWN_END = 0.8;\nconst LIFE = 0.3;\nconst MIN_LIFE = 3;\nconst DRAW = 0.4;\nconst ERASE = 0.35;\nconst SHUTTER = 1.6;\nconst DRIFT = 0.15;\nconst SETBACK = 0.35;\nconst SLOW = 5;\nconst FAST = 14;\nconst GOLDEN = (Math.sqrt(5) - 1) / 2;\nconst IMPACT = 3;\nconst RUSH_JITTER = 1;\nconst RELAX = 0.12;\nconst RELAX_RATE = 1.5;\nconst RELEASE = 8;\nconst RELEASE_JITTER = 1.5;\nconst RETRACT = 3.5;\nconst FOCUS_END = RELEASE + RELEASE_JITTER + RETRACT;\nconst ANGLE_JITTER = 0.6;\nconst RADIUS_JITTER = 0.6;\nconst MAX_COUNT = 64;\nconst MIN_WEIGHT = 0.5;\nconst MAX_WEIGHT = 12;\nconst MIN_SPREAD = 8;\nconst MAX_SPREAD = 480;\nconst MIN_DURATION = 4;\nconst MAX_DURATION = 120;\n\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst round = (value: number) => Math.round(value * 100) / 100;\nconst fine = (value: number) => Math.round(value * 10000) / 10000;\nconst fraction = (value: number) => value - Math.floor(value);\nconst modeOf = (value: string | undefined): SpeedLinesMode =>\n  value === \"focus\" ? \"focus\" : \"trail\";\nconst travelOf = (value: number | undefined) =>\n  clamp(finite(value, speedLinesDefaults.duration), MIN_DURATION, MAX_DURATION);\n\nfunction smoothstep(value: number) {\n  const t = clamp(value, 0, 1);\n  return t * t * (3 - 2 * t);\n}\n\nfunction easeOut(value: number) {\n  const t = clamp(value, 0, 1);\n  return 1 - (1 - t) * (1 - t);\n}\n\nfunction pointOf(\n  value: SpeedLinesPoint | undefined,\n  fallback: SpeedLinesPoint,\n) {\n  return { x: finite(value?.x, fallback.x), y: finite(value?.y, fallback.y) };\n}\n\nfunction key(seed: number, index: number, name: string) {\n  return random(`speed-lines-${seed}-${index}-${name}`);\n}\n\nfunction settingsOf(options: SpeedLinesOptions) {\n  const height = Math.max(1, finite(options.height, 720));\n  const x = finite(options.x, speedLinesDefaults.x);\n  const y = finite(options.y, speedLinesDefaults.y);\n  const count = finite(options.count, speedLinesDefaults.count);\n  const weight = finite(options.weight, speedLinesDefaults.weight);\n  const spread = finite(options.spread, speedLinesDefaults.spread);\n  return {\n    mode: modeOf(options.mode),\n    width: Math.max(1, finite(options.width, 1280)),\n    height,\n    fps: Math.max(1, finite(options.fps, 30)),\n    unit: height / REFERENCE,\n    from: pointOf(options.from, speedLinesDefaults.from),\n    to: pointOf(options.to, { x, y }),\n    x,\n    y,\n    count: Math.round(clamp(count, 1, MAX_COUNT)),\n    weight: clamp(weight, MIN_WEIGHT, MAX_WEIGHT),\n    spread: clamp(spread, MIN_SPREAD, MAX_SPREAD),\n    duration: travelOf(options.duration),\n    bounce: clamp(finite(options.bounce, speedLinesDefaults.bounce), 0, 1),\n    squash: clamp(finite(options.squash, speedLinesDefaults.squash), 0, 1),\n    seed: finite(options.seed, speedLinesDefaults.seed),\n    delay: Math.max(0, finite(options.delay, speedLinesDefaults.delay)),\n    speed: Math.max(0, finite(options.speed, speedLinesDefaults.speed)),\n  };\n}\n\ntype Settings = ReturnType<typeof settingsOf>;\n\nfunction directionOf(settings: Settings) {\n  const { from, to, width, height } = settings;\n  const dx = (to.x - from.x) * width;\n  const dy = (to.y - from.y) * height;\n  const distance = Math.hypot(dx, dy);\n  if (distance < 1e-6) return { dx: 1, dy: 0, distance: 0 };\n  return { dx: dx / distance, dy: dy / distance, distance };\n}\n\nfunction along(position: number, size: number, step: number) {\n  if (step > 1e-9) return (size - position) / step;\n  if (step < -1e-9) return -position / step;\n  return Number.POSITIVE_INFINITY;\n}\n\nfunction exitDistance(\n  cx: number,\n  cy: number,\n  cos: number,\n  sin: number,\n  width: number,\n  height: number,\n) {\n  const inside = cx >= 0 && cx <= width && cy >= 0 && cy <= height;\n  if (!inside) {\n    const reachX = Math.max(Math.abs(cx), Math.abs(width - cx));\n    const reachY = Math.max(Math.abs(cy), Math.abs(height - cy));\n    return Math.hypot(reachX, reachY);\n  }\n  return Math.min(along(cx, width, cos), along(cy, height, sin));\n}\n\nfunction springOf(bounce: number) {\n  const landing = LANDING + CARRY * bounce;\n  const kappa = (1 + Math.sqrt(1 - landing)) / 2;\n  const gain = 1 / (0.5 - kappa / 3);\n  const echo = ECHO * bounce;\n  const decrement = -Math.log(echo);\n  const damping = echo > 0 ? decrement / Math.hypot(Math.PI, decrement) : 1;\n  const natural = 1 / (1 + SOFTEN * bounce);\n  const decay = damping * natural;\n  return {\n    kappa,\n    gain,\n    contact: gain * (1 - kappa),\n    decay,\n    ring: natural * Math.sqrt(1 - damping * damping),\n    settle: Math.ceil(SETTLE / decay),\n  };\n}\n\nfunction easeOf(time: number, travel: number, bounce: number) {\n  if (time <= 0) return 0;\n  const { kappa, gain, contact, decay, ring, settle } = springOf(bounce);\n  if (time < travel) {\n    const w = time / travel;\n    return gain * ((w * w) / 2 - (kappa * w * w * w) / 3);\n  }\n  const since = time - travel;\n  if (since >= settle) return 1;\n  const taper = smoothstep((2 * since) / settle - 1);\n  const wave = ring > 1e-9 ? Math.sin(ring * since) / ring : since;\n  const envelope = Math.exp(-decay * since) * (1 - taper);\n  return 1 + (contact / travel) * wave * envelope;\n}\n\nexport function getSpeedLinesTimeline(options: SpeedLinesOptions = {}) {\n  const { mode, duration, bounce } = settingsOf(options);\n  if (mode === \"focus\") return { mode, impact: IMPACT, end: FOCUS_END };\n  return { mode, impact: duration, end: duration + springOf(bounce).settle };\n}\n\nexport type SpeedLinesTimeline = ReturnType<typeof getSpeedLinesTimeline>;\n\nexport const speedLinesLength = getSpeedLinesTimeline().end;\n\nexport function getSpeedLinesDuration(options: SpeedLinesOptions = {}) {\n  const { delay, speed } = settingsOf(options);\n  if (speed === 0) return 1;\n  const { end } = getSpeedLinesTimeline(options);\n  return Math.max(1, Math.ceil(delay + (end + HOLD) / speed));\n}\n\nexport function getSpeedLinesTime(\n  frame: number,\n  options: SpeedLinesOptions = {},\n) {\n  const { fps, delay, speed } = settingsOf(options);\n  const elapsed = Math.max(0, finite(frame, 0) - delay);\n  return elapsed * (30 / fps) * speed;\n}\n\nexport function getSpeedLinesEase(\n  time: number,\n  options: SpeedLinesOptions = {},\n) {\n  const { duration, bounce } = settingsOf(options);\n  return easeOf(finite(time, 0), duration, bounce);\n}\n\nfunction positionAt(time: number, settings: Settings) {\n  const { mode, from, to, width, height, duration, bounce } = settings;\n  if (mode === \"focus\") {\n    return { x: settings.x * width, y: settings.y * height, progress: 1 };\n  }\n  const progress = easeOf(time, duration, bounce);\n  const startX = from.x * width;\n  const startY = from.y * height;\n  return {\n    x: startX + (to.x * width - startX) * progress,\n    y: startY + (to.y * height - startY) * progress,\n    progress,\n  };\n}\n\nexport function getSpeedLinesPosition(\n  time: number,\n  options: SpeedLinesOptions = {},\n) {\n  const { x, y } = positionAt(finite(time, 0), settingsOf(options));\n  return { x, y };\n}\n\nfunction shapeAt(time: number, settings: Settings) {\n  const { mode, duration, bounce, squash, unit, speed } = settings;\n  if (mode === \"focus\") return { rotation: 0, along: 1, across: 1 };\n  const { dx, dy, distance } = directionOf(settings);\n  const rotation = (Math.atan2(dy, dx) * 180) / Math.PI;\n  if (squash === 0 || distance === 0 || time <= 0) {\n    return { rotation, along: 1, across: 1 };\n  }\n  const span = distance / unit;\n  const ahead = easeOf(time + PROBE, duration, bounce);\n  const behind = easeOf(time - PROBE, duration, bounce);\n  const rate = (Math.abs(ahead - behind) / (2 * PROBE)) * span * speed;\n  const landed = time > duration ? easeOf(time, duration, bounce) - 1 : 0;\n  const past = Math.max(0, landed) * span;\n  const stretch = STRETCH * Math.tanh((squash * STRETCH_GAIN * rate) / STRETCH);\n  const compression =\n    SQUASH * Math.tanh((squash * SQUASH_GAIN * past) / SQUASH);\n  const strain = clamp(stretch - compression, -SQUASH, STRETCH);\n  return { rotation, along: Math.exp(strain), across: Math.exp(-strain) };\n}\n\nexport function getSpeedLinesShape(\n  time: number,\n  options: SpeedLinesOptions = {},\n) {\n  return shapeAt(finite(time, 0), settingsOf(options));\n}\n\nexport type SpeedLinesShape = ReturnType<typeof getSpeedLinesShape>;\n\nexport function getSpeedLinesVelocity(\n  frame: number,\n  options: SpeedLinesOptions = {},\n) {\n  const settings = settingsOf(options);\n  const current = finite(frame, 0);\n  const now = positionAt(getSpeedLinesTime(current, options), settings);\n  const before = positionAt(getSpeedLinesTime(current - 1, options), settings);\n  const scale = settings.fps / 30;\n  const x = (now.x - before.x) * scale;\n  const y = (now.y - before.y) * scale;\n  const { dx, dy } = directionOf(settings);\n  return { x, y, forward: x * dx + y * dy };\n}\n\nfunction streakSeeds(settings: Settings) {\n  const { seed, count, spread, unit } = settings;\n  const band = spread * unit;\n  const phase = random(`speed-lines-${seed}-lanes`);\n  return Array.from({ length: count }, (_, index) => {\n    const jitter = (key(seed, index, \"lane\") - 0.5) / (2 * count);\n    const lane = fraction(phase + GOLDEN * index + jitter);\n    const slot = (index + key(seed, index, \"spawn\")) / count;\n    return {\n      index,\n      spawn: SPAWN_START + (SPAWN_END - SPAWN_START) * slot,\n      offset: band * (lane - 0.5),\n      factor: 0.5 + 0.5 * key(seed, index, \"length\"),\n      setback: SETBACK * band * key(seed, index, \"setback\"),\n    };\n  });\n}\n\nexport function getSpeedLinesStreaks(\n  frame: number,\n  options: SpeedLinesOptions = {},\n): SpeedLinesStreak[] {\n  const settings = settingsOf(options);\n  if (settings.mode !== \"trail\") return [];\n  const time = getSpeedLinesTime(frame, options);\n  const { dx, dy, distance } = directionOf(settings);\n  if (distance === 0 || time <= 0) return [];\n  const { forward } = getSpeedLinesVelocity(frame, options);\n  const { unit, duration, bounce, weight } = settings;\n  const fast = smoothstep((forward - SLOW * unit) / ((FAST - SLOW) * unit));\n  const gate = fast * (1 - smoothstep(time - duration));\n  if (gate <= 0) return [];\n  const center = positionAt(time, settings);\n  const progress = time / duration;\n  const life = Math.max(MIN_LIFE, LIFE * duration) / duration;\n  const minimum = 0.5 * weight * unit;\n  const streaks: SpeedLinesStreak[] = [];\n  for (const streak of streakSeeds(settings)) {\n    const age = (progress - streak.spawn) / life;\n    if (age <= 0 || age >= 1) continue;\n    const reach = SHUTTER * streak.factor * forward * gate;\n    const drawn = easeOut(age / DRAW);\n    const erased = smoothstep((age - ERASE) / (1 - ERASE));\n    const length = reach * (drawn - erased);\n    if (length < minimum) continue;\n    const spawned = easeOf(streak.spawn * duration, duration, bounce);\n    const since = Math.max(0, (center.progress - spawned) * distance);\n    const base = streak.setback + DRIFT * since;\n    const front = base + reach * erased;\n    const back = base + reach * drawn;\n    const laneX = center.x - dy * streak.offset;\n    const laneY = center.y + dx * streak.offset;\n    streaks.push({\n      index: streak.index,\n      offset: streak.offset,\n      reach,\n      x1: round(laneX - dx * front),\n      y1: round(laneY - dy * front),\n      x2: round(laneX - dx * back),\n      y2: round(laneY - dy * back),\n      length,\n    });\n  }\n  return streaks;\n}\n\nexport function getSpeedLinesRadius(time: number, ray: SpeedLinesRayTiming) {\n  const { rest, outer, start = 0, release = RELEASE } = ray;\n  const t = finite(time, 0);\n  if (t <= start) return outer;\n  if (t < IMPACT) {\n    const q = (t - start) / (IMPACT - start);\n    return outer + (rest - outer) * q * q;\n  }\n  const settle = (at: number) =>\n    rest * (1 + RELAX * (1 - Math.exp(-(at - IMPACT) / RELAX_RATE)));\n  if (t < release) return settle(t);\n  const held = settle(release);\n  const q = clamp((t - release) / RETRACT, 0, 1);\n  return outer - (outer - held) * (1 - q * q);\n}\n\nexport function getSpeedLinesRays(\n  frame: number,\n  options: SpeedLinesOptions = {},\n): SpeedLinesRay[] {\n  const settings = settingsOf(options);\n  if (settings.mode !== \"focus\") return [];\n  const time = getSpeedLinesTime(frame, options);\n  if (time <= 0 || time >= FOCUS_END) return [];\n  const { seed, count, spread, weight, unit, width, height } = settings;\n  const cx = settings.x * width;\n  const cy = settings.y * height;\n  const minimum = 0.5 * weight * unit;\n  const rotation = random(`speed-lines-${seed}-rotation`);\n  const rays: SpeedLinesRay[] = [];\n  for (let index = 0; index < count; index++) {\n    const jitter = ANGLE_JITTER * (key(seed, index, \"angle\") - 0.5);\n    const angle = 2 * Math.PI * (rotation + (index + jitter) / count);\n    const cos = Math.cos(angle);\n    const sin = Math.sin(angle);\n    const reach = RADIUS_JITTER * key(seed, index, \"radius\");\n    const rest = spread * unit * (1 + reach);\n    const outer = exitDistance(cx, cy, cos, sin, width, height) + weight * unit;\n    const inner = getSpeedLinesRadius(time, {\n      rest,\n      outer,\n      start: RUSH_JITTER * key(seed, index, \"rush\"),\n      release: RELEASE + RELEASE_JITTER * key(seed, index, \"release\"),\n    });\n    const length = outer - inner;\n    if (length < minimum) continue;\n    rays.push({\n      index,\n      angle,\n      rest,\n      inner,\n      outer,\n      x1: round(cx + cos * inner),\n      y1: round(cy + sin * inner),\n      x2: round(cx + cos * outer),\n      y2: round(cy + sin * outer),\n      length,\n    });\n  }\n  return rays;\n}\n\nexport function getSpeedLinesState(\n  frame: number,\n  options: SpeedLinesOptions = {},\n) {\n  const settings = settingsOf(options);\n  const time = getSpeedLinesTime(frame, options);\n  const point = positionAt(time, settings);\n  const shape = shapeAt(time, settings);\n  const rotation = round(shape.rotation);\n  const scale = `scale(${fine(shape.along)}, ${fine(shape.across)})`;\n  const streaks = getSpeedLinesStreaks(frame, options);\n  const rays = getSpeedLinesRays(frame, options);\n  const path = [...streaks, ...rays]\n    .map(({ x1, y1, x2, y2 }) => `M ${x1} ${y1} L ${x2} ${y2}`)\n    .join(\" \");\n  return {\n    mode: settings.mode,\n    width: settings.width,\n    height: settings.height,\n    unit: settings.unit,\n    time,\n    x: point.x,\n    y: point.y,\n    left: (point.x / settings.width) * 100,\n    top: (point.y / settings.height) * 100,\n    radius: (settings.spread * settings.unit) / 2,\n    strokeWidth: settings.weight * settings.unit,\n    rotation,\n    along: shape.along,\n    across: shape.across,\n    transform:\n      shape.along === 1\n        ? CENTER\n        : `${CENTER} rotate(${rotation}deg) ${scale} rotate(${-rotation}deg)`,\n    streaks,\n    rays,\n    path,\n  };\n}\n\nexport type SpeedLinesState = ReturnType<typeof getSpeedLinesState>;\n\nexport function SpeedLines({\n  mode = speedLinesDefaults.mode,\n  from = speedLinesDefaults.from,\n  to,\n  x = speedLinesDefaults.x,\n  y = speedLinesDefaults.y,\n  count = speedLinesDefaults.count,\n  weight = speedLinesDefaults.weight,\n  spread = speedLinesDefaults.spread,\n  duration = speedLinesDefaults.duration,\n  bounce = speedLinesDefaults.bounce,\n  squash = speedLinesDefaults.squash,\n  color = speedLinesDefaults.color,\n  seed = speedLinesDefaults.seed,\n  delay = speedLinesDefaults.delay,\n  speed = speedLinesDefaults.speed,\n  children,\n  className,\n}: SpeedLinesProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const state = getSpeedLinesState(frame, {\n    mode,\n    from,\n    to,\n    x,\n    y,\n    count,\n    weight,\n    spread,\n    duration,\n    bounce,\n    squash,\n    seed,\n    delay,\n    speed,\n    width,\n    height,\n    fps,\n  });\n  const fallback = state.mode === \"trail\" && children === undefined;\n  const placed = children !== undefined && children !== null;\n\n  return (\n    <div\n      className={className}\n      style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n    >\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={`0 0 ${width} ${height}`}\n        aria-hidden=\"true\"\n        style={{ position: \"absolute\", inset: 0, display: \"block\" }}\n      >\n        {state.path ? (\n          <path\n            d={state.path}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={state.strokeWidth}\n            strokeLinecap=\"round\"\n          />\n        ) : null}\n        {fallback ? (\n          <ellipse\n            cx={state.x}\n            cy={state.y}\n            rx={state.radius * state.along}\n            ry={state.radius * state.across}\n            transform={`rotate(${state.rotation} ${state.x} ${state.y})`}\n            fill={color}\n          />\n        ) : null}\n      </svg>\n      {placed ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: `${state.left}%`,\n            top: `${state.top}%`,\n            width: \"max-content\",\n            transform: state.transform,\n            transformOrigin: \"50% 50%\",\n          }}\n        >\n          {children}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/speed-lines.tsx"
    }
  ],
  "type": "registry:component"
}
