{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "squiggle",
  "title": "Squiggle",
  "description": "A flat line draws itself along a straight course as a traveling wave whose crests swim forward and calm down as the head runs out, in wave, zigzag, loop or coil shapes, then erases toward its end point.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/squiggle/index.tsx",
      "content": "\"use client\";\n\nimport { useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type SquiggleShape = \"wave\" | \"zigzag\" | \"loops\" | \"coil\";\nexport type SquiggleJoin = \"miter\" | \"round\";\n\nexport interface SquiggleProps {\n  shape?: SquiggleShape;\n  x?: number;\n  y?: number;\n  length?: number;\n  angle?: number;\n  amplitude?: number;\n  wavelength?: number;\n  weight?: number;\n  color?: string;\n  settle?: number;\n  hold?: number;\n  erase?: boolean;\n  delay?: number;\n  speed?: number;\n  className?: string;\n}\n\ntype Paint = \"color\" | \"className\";\n\nexport interface SquiggleOptions extends Omit<SquiggleProps, Paint> {\n  width?: number;\n  height?: number;\n  fps?: number;\n}\n\nexport interface SquigglePoint {\n  x: number;\n  y: number;\n}\n\nexport interface SquiggleCurveInput {\n  shape: SquiggleShape;\n  length: number;\n  wavelength: number;\n  amplitude: number;\n  phase: number;\n  unit?: number;\n}\n\nexport const squiggleDefaults: Required<Omit<SquiggleProps, \"className\">> = {\n  shape: \"wave\",\n  x: 0.375,\n  y: 0.5,\n  length: 320,\n  angle: 0,\n  amplitude: 14,\n  wavelength: 48,\n  weight: 3,\n  color: \"#ffffff\",\n  settle: 0.5,\n  hold: 12,\n  erase: true,\n  delay: 0,\n  speed: 1,\n};\n\ntype SmoothShape = Exclude<SquiggleShape, \"zigzag\">;\n\nconst DRAW = 24;\nconst SETTLE = 36;\nconst ERASE = 18;\nconst SWIM = 1.5;\nconst EXIT_SWIM = 0.5;\nconst TAIL = 14;\nconst REST = 24;\nconst MIN_WAVELENGTH = 8;\nconst LOOP_FLOOR = 3;\nconst MIN_POINTS = 24;\nconst MAX_POINTS = 4000;\nconst EPSILON = 0.01;\nconst SAMPLES: Record<SmoothShape, number> = { wave: 32, loops: 64, coil: 80 };\nconst REACH: Record<SmoothShape, number> = { wave: 0, loops: 2.4, coil: 4.2 };\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 rateOf = (speed: number | undefined) =>\n  Math.max(0, finite(speed, squiggleDefaults.speed));\nconst shapeOf = (value: string | undefined): SquiggleShape =>\n  value === \"zigzag\" || value === \"loops\" || value === \"coil\" ? value : \"wave\";\nconst joinOf = (shape: SquiggleShape): SquiggleJoin =>\n  shape === \"zigzag\" ? \"miter\" : \"round\";\nconst easeOutQuart = (t: number) => 1 - (1 - t) ** 4;\nconst easeOutCubic = (t: number) => 1 - (1 - t) ** 3;\nconst easeInOutCubic = (t: number) =>\n  t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;\nconst easeInOutSine = (t: number) => (1 - Math.cos(Math.PI * t)) / 2;\n\nfunction smoothstep(value: number) {\n  const t = clamp(value, 0, 1);\n  return t * t * (3 - 2 * t);\n}\n\nexport function getSquiggleTimeline(\n  options: Pick<SquiggleOptions, \"hold\" | \"erase\"> = {},\n) {\n  const hold = Math.max(0, finite(options.hold, squiggleDefaults.hold));\n  const erase = options.erase !== false;\n  const eraseStart = SETTLE + hold;\n  const eraseEnd = eraseStart + ERASE;\n  const end = erase ? eraseEnd : SETTLE;\n  return {\n    draw: DRAW,\n    settle: SETTLE,\n    hold,\n    erase,\n    eraseStart,\n    eraseEnd,\n    end,\n    duration: end + (erase ? TAIL : REST),\n  };\n}\n\nexport type SquiggleTimeline = ReturnType<typeof getSquiggleTimeline>;\n\nexport const squiggleLength = getSquiggleTimeline().end;\n\nexport function getSquiggleDuration(\n  options: Pick<SquiggleOptions, \"hold\" | \"erase\" | \"speed\" | \"delay\"> = {},\n) {\n  const rate = rateOf(options.speed);\n  if (rate === 0) return 1;\n  const delay = Math.max(0, finite(options.delay, squiggleDefaults.delay));\n  const { duration } = getSquiggleTimeline(options);\n  return Math.max(1, Math.ceil(delay + duration / rate));\n}\n\nexport function getSquiggleTime(\n  frame: number,\n  options: Pick<SquiggleOptions, \"fps\" | \"speed\" | \"delay\"> = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const delay = Math.max(0, finite(options.delay, squiggleDefaults.delay));\n  const elapsed = Math.max(0, finite(frame, 0) - delay);\n  return (elapsed * 30 * rateOf(options.speed)) / fps;\n}\n\nexport function getSquiggleHead(time: number) {\n  if (!(time > 0)) return 0;\n  return easeOutQuart(Math.min(1, time / DRAW));\n}\n\nexport function getSquiggleTail(\n  time: number,\n  timeline: SquiggleTimeline = getSquiggleTimeline(),\n) {\n  if (!timeline.erase) return 0;\n  const progress = (time - timeline.eraseStart) / ERASE;\n  if (!(progress > 0)) return 0;\n  return easeInOutCubic(Math.min(1, progress));\n}\n\nexport function getSquiggleCalm(time: number) {\n  if (!(time > 0)) return 0;\n  return easeInOutSine(Math.min(1, time / SETTLE));\n}\n\nexport function getSquigglePhase(\n  time: number,\n  timeline: SquiggleTimeline = getSquiggleTimeline(),\n) {\n  const swim = time > 0 ? easeOutCubic(Math.min(1, time / SETTLE)) : 0;\n  return SWIM * swim + EXIT_SWIM * getSquiggleTail(time, timeline);\n}\n\nexport function getSquiggleEnvelope(\n  s: number,\n  length: number,\n  wavelength: number,\n) {\n  const taper = Math.min(wavelength, length / 3);\n  if (!(taper > 0)) return 0;\n  return smoothstep(s / taper) * smoothstep((length - s) / taper);\n}\n\nfunction getZigzagCurve(\n  length: number,\n  wavelength: number,\n  amplitude: number,\n  shift: number,\n) {\n  const k = (2 * Math.PI) / wavelength;\n  const points: SquigglePoint[] = [{ x: 0, y: 0 }];\n  const first = Math.floor(-(Math.PI / 2 + shift) / Math.PI) + 1;\n  for (let m = first; points.length < MAX_POINTS - 1; m++) {\n    const s = (Math.PI / 2 + m * Math.PI + shift) / k;\n    if (s >= length) break;\n    if (s <= 0) continue;\n    const side = m % 2 === 0 ? 1 : -1;\n    const w = getSquiggleEnvelope(s, length, wavelength);\n    points.push({ x: s, y: side * amplitude * w });\n  }\n  points.push({ x: length, y: 0 });\n  return points;\n}\n\nexport function getSquiggleCurve(input: SquiggleCurveInput) {\n  const shape = shapeOf(input.shape);\n  const length = Math.max(0, finite(input.length, 0));\n  const wavelength = Math.max(1e-3, finite(input.wavelength, 1));\n  const amplitude = Math.max(0, finite(input.amplitude, 0));\n  const unit = Math.max(1e-3, finite(input.unit, 1));\n  const shift = 2 * Math.PI * finite(input.phase, 0);\n  if (shape === \"zigzag\") {\n    return getZigzagCurve(length, wavelength, amplitude, shift);\n  }\n  const k = (2 * Math.PI) / wavelength;\n  const gate = smoothstep(amplitude / (LOOP_FLOOR * unit));\n  const reach = (REACH[shape] / k) * gate;\n  const fine = wavelength / (SAMPLES[shape] * Math.max(1, unit));\n  const coarsest = Math.max(length / MIN_POINTS, 1e-6);\n  const step = clamp(fine, length / (MAX_POINTS - 2), coarsest);\n  const sample = (s: number): SquigglePoint => {\n    const w = getSquiggleEnvelope(s, length, wavelength);\n    const theta = k * s - shift;\n    if (shape === \"wave\") return { x: s, y: amplitude * w * Math.sin(theta) };\n    const along = s - reach * w * Math.sin(theta);\n    return { x: along, y: -amplitude * w * Math.cos(theta) };\n  };\n  const offset = (((shift / k) % step) + step) % step;\n  const points = [sample(0)];\n  for (let j = 0; points.length < MAX_POINTS - 1; j++) {\n    const s = offset + j * step;\n    if (s >= length) break;\n    if (s > 0) points.push(sample(s));\n  }\n  points.push(sample(length));\n  return points;\n}\n\nexport function getSquiggleTrim(\n  points: readonly SquigglePoint[],\n  tail: number,\n  head: number,\n) {\n  const lengths = [0];\n  for (let i = 1; i < points.length; i++) {\n    const a = points[i - 1];\n    const b = points[i];\n    lengths.push(lengths[i - 1] + Math.hypot(b.x - a.x, b.y - a.y));\n  }\n  const last = points.length - 1;\n  const total = last > 0 ? lengths[last] : 0;\n  const from = clamp(finite(tail, 0), 0, 1) * total;\n  const to = clamp(finite(head, 0), 0, 1) * total;\n  const trimmed: SquigglePoint[] = [];\n  if (last < 1 || !(to - from > EPSILON)) return { total, points: trimmed };\n  const pointAt = (distance: number, index: number): SquigglePoint => {\n    const a = points[index - 1];\n    const b = points[index];\n    const span = lengths[index] - lengths[index - 1];\n    const t = span > 0 ? (distance - lengths[index - 1]) / span : 0;\n    return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };\n  };\n  let index = 1;\n  while (index < last && lengths[index] < from) index++;\n  trimmed.push(pointAt(from, index));\n  while (index < last && lengths[index] < to) {\n    trimmed.push(points[index]);\n    index++;\n  }\n  trimmed.push(pointAt(to, index));\n  return { total, points: trimmed };\n}\n\nexport function getSquigglePath(points: readonly SquigglePoint[]) {\n  let path = \"\";\n  let previous = \"\";\n  for (const point of points) {\n    const pair = `${round(point.x)} ${round(point.y)}`;\n    if (pair === previous) continue;\n    path += `${path === \"\" ? \"M\" : \"L\"}${pair}`;\n    previous = pair;\n  }\n  return path;\n}\n\nexport function getSquiggleState(frame: number, options: SquiggleOptions = {}) {\n  const width = Math.max(1, finite(options.width, 1280));\n  const height = Math.max(1, finite(options.height, 720));\n  const unit = height / 720;\n  const shape = shapeOf(options.shape);\n  const timeline = getSquiggleTimeline(options);\n  const time = getSquiggleTime(frame, options);\n  const head = getSquiggleHead(time);\n  const tail = getSquiggleTail(time, timeline);\n  const phase = getSquigglePhase(time, timeline);\n  const settle = clamp(finite(options.settle, squiggleDefaults.settle), 0, 1);\n  const lively = finite(options.amplitude, squiggleDefaults.amplitude);\n  const calm = 1 - settle * getSquiggleCalm(time);\n  const amplitude = Math.max(0, lively) * unit * calm;\n  const length = Math.max(0, finite(options.length, squiggleDefaults.length));\n  const course = length * unit;\n  const cycle = finite(options.wavelength, squiggleDefaults.wavelength);\n  const wavelength = Math.max(MIN_WAVELENGTH, cycle) * unit;\n  const weight = Math.max(0, finite(options.weight, squiggleDefaults.weight));\n  const angle = finite(options.angle, squiggleDefaults.angle);\n  const radians = (angle * Math.PI) / 180;\n  const cos = Math.cos(radians);\n  const sin = Math.sin(radians);\n  const start = {\n    x: finite(options.x, squiggleDefaults.x) * width,\n    y: finite(options.y, squiggleDefaults.y) * height,\n  };\n  const end = { x: start.x + course * cos, y: start.y + course * sin };\n  const drawn = head - tail > 0;\n  const curve: SquigglePoint[] = drawn\n    ? getSquiggleCurve({\n        shape,\n        length: course,\n        wavelength,\n        amplitude,\n        phase,\n        unit,\n      })\n    : [];\n  const placed = curve.map((point) => ({\n    x: start.x + point.x * cos - point.y * sin,\n    y: start.y + point.x * sin + point.y * cos,\n  }));\n  const trim = getSquiggleTrim(placed, tail, head);\n  const path = getSquigglePath(trim.points);\n  return {\n    width,\n    height,\n    unit,\n    shape,\n    time,\n    timeline,\n    head,\n    tail,\n    phase,\n    amplitude,\n    wavelength,\n    length: course,\n    strokeWidth: round(weight * unit),\n    join: joinOf(shape),\n    start,\n    end,\n    total: trim.total,\n    points: trim.points,\n    path,\n    visible: path.includes(\"L\"),\n  };\n}\n\nexport type SquiggleState = ReturnType<typeof getSquiggleState>;\n\nexport function Squiggle({\n  shape = squiggleDefaults.shape,\n  x = squiggleDefaults.x,\n  y = squiggleDefaults.y,\n  length = squiggleDefaults.length,\n  angle = squiggleDefaults.angle,\n  amplitude = squiggleDefaults.amplitude,\n  wavelength = squiggleDefaults.wavelength,\n  weight = squiggleDefaults.weight,\n  color = squiggleDefaults.color,\n  settle = squiggleDefaults.settle,\n  hold = squiggleDefaults.hold,\n  erase = squiggleDefaults.erase,\n  delay = squiggleDefaults.delay,\n  speed = squiggleDefaults.speed,\n  className,\n}: SquiggleProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const state = getSquiggleState(frame, {\n    shape,\n    x,\n    y,\n    length,\n    angle,\n    amplitude,\n    wavelength,\n    weight,\n    settle,\n    hold,\n    erase,\n    delay,\n    speed,\n    width,\n    height,\n    fps,\n  });\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        role=\"img\"\n        aria-label=\"Squiggle line\"\n        style={{ display: \"block\" }}\n      >\n        {state.visible ? (\n          <path\n            d={state.path}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={state.strokeWidth}\n            strokeLinecap=\"round\"\n            strokeLinejoin={state.join}\n          />\n        ) : null}\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/squiggle.tsx"
    }
  ],
  "type": "registry:component"
}
