{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trim-burst",
  "title": "Trim Burst",
  "description": "A ring of short straight strokes shoots out from a point like Trim Paths: each end races ahead on a hard-braking ease-out while its start holds, then chases it on a smoother ease-in-out, so every stroke grows, travels and zips shut at its outer distance.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/trim-burst/index.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Easing, random, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type TrimBurstCap = \"round\" | \"butt\";\n\nexport interface TrimBurstProps {\n  x?: number;\n  y?: number;\n  count?: number;\n  innerRadius?: number;\n  reach?: number;\n  strokeLength?: number;\n  weight?: number;\n  rotation?: number;\n  spread?: number;\n  jitter?: number;\n  dots?: boolean;\n  cap?: TrimBurstCap;\n  color?: string;\n  seed?: number;\n  delay?: number;\n  speed?: number;\n  className?: string;\n}\n\ntype Paint = \"color\" | \"className\";\n\nexport interface TrimBurstOptions extends Omit<TrimBurstProps, Paint> {\n  width?: number;\n  height?: number;\n  fps?: number;\n}\n\nexport interface TrimBurstStroke {\n  index: number;\n  angle: number;\n  scale: number;\n  launch: number;\n}\n\nexport interface TrimBurstTrim {\n  start: number;\n  end: number;\n  length: number;\n}\n\nexport interface TrimBurstLine {\n  index: number;\n  x1: number;\n  y1: number;\n  x2: number;\n  y2: number;\n  width: number;\n}\n\nexport interface TrimBurstDot {\n  index: number;\n  cx: number;\n  cy: number;\n  r: number;\n}\n\nexport const trimBurstDefaults: Required<Omit<TrimBurstProps, \"className\">> = {\n  x: 0.5,\n  y: 0.5,\n  count: 8,\n  innerRadius: 16,\n  reach: 64,\n  strokeLength: 56,\n  weight: 2,\n  rotation: 0,\n  spread: 360,\n  jitter: 0.5,\n  dots: false,\n  cap: \"round\",\n  color: \"#ffffff\",\n  seed: 1,\n  delay: 0,\n  speed: 1,\n};\n\nconst LIFE = 15;\nconst LAG = 3;\nconst STAGGER = 6;\nconst HOLD = 24;\nconst LEASH = 6;\nconst WOBBLE = 0.25;\nconst MAX_STEP = 45;\nconst SHRINK = 0.45;\nconst DOT_START = 4;\nconst DOT_POP = 4;\nconst DOT_BACK = 1.4;\nconst DOT_RADIUS = 1.25;\nconst DOT_GAP = 2;\nconst MAX_COUNT = 32;\nconst MAX_SIZE = 2000;\nconst MIN_STROKE = 1;\nconst MIN_WEIGHT = 0.25;\nconst MAX_WEIGHT = 24;\nconst VISIBLE = 0.001;\nconst END_EASE = Easing.bezier(0.16, 1, 0.3, 1);\nconst START_EASE = Easing.bezier(0.45, 0, 0.55, 1);\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 within = (\n  value: number | undefined,\n  fallback: number,\n  min: number,\n  max: number,\n) => clamp(finite(value, fallback), min, max);\nconst round = (value: number) => Math.round(value * 1000) / 1000;\nconst rateOf = (speed: number | undefined) =>\n  Math.max(0, finite(speed, trimBurstDefaults.speed));\nconst delayOf = (delay: number | undefined) =>\n  Math.max(0, finite(delay, trimBurstDefaults.delay));\nconst countOf = (count: number | undefined) =>\n  Math.round(within(count, trimBurstDefaults.count, 1, MAX_COUNT));\nconst jitterOf = (jitter: number | undefined) =>\n  within(jitter, trimBurstDefaults.jitter, 0, 1);\nconst capOf = (cap: string | undefined): TrimBurstCap =>\n  cap === \"butt\" ? cap : \"round\";\n\nexport function getTrimBurstTimeline(\n  options: Pick<TrimBurstOptions, \"count\" | \"jitter\"> = {},\n) {\n  const count = countOf(options.count);\n  const stagger = count > 1 ? STAGGER * jitterOf(options.jitter) : 0;\n  return { life: LIFE, lag: LAG, stagger, length: LIFE + stagger };\n}\n\nexport type TrimBurstTimeline = ReturnType<typeof getTrimBurstTimeline>;\n\nexport const trimBurstLength = getTrimBurstTimeline().length;\n\nexport function getTrimBurstDuration(\n  options: Pick<TrimBurstOptions, \"count\" | \"jitter\" | \"delay\" | \"speed\"> = {},\n) {\n  const rate = rateOf(options.speed);\n  const { length } = getTrimBurstTimeline(options);\n  const motion = rate === 0 ? 0 : length / rate;\n  return Math.ceil(round(delayOf(options.delay) + motion)) + HOLD;\n}\n\nexport function getTrimBurstTime(\n  frame: number,\n  options: Pick<TrimBurstOptions, \"fps\" | \"delay\" | \"speed\"> = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const elapsed = Math.max(0, finite(frame, 0) - delayOf(options.delay));\n  return elapsed * (30 / fps) * rateOf(options.speed);\n}\n\nexport function getTrimBurstEndProgress(time: number) {\n  if (Number.isNaN(time) || time <= 0) return 0;\n  if (time >= LIFE) return 1;\n  return END_EASE(time / LIFE);\n}\n\nexport function getTrimBurstStartProgress(time: number) {\n  if (Number.isNaN(time) || time <= LAG) return 0;\n  if (time >= LIFE) return 1;\n  return START_EASE((time - LAG) / (LIFE - LAG));\n}\n\nexport function getTrimBurstLeash(gap: number, strokeLength: number) {\n  if (!(gap > 0 && strokeLength > 0)) return 0;\n  return gap / (1 + (gap / strokeLength) ** LEASH) ** (1 / LEASH);\n}\n\nexport function getTrimBurstTrim(\n  time: number,\n  reach: number,\n  strokeLength: number,\n): TrimBurstTrim {\n  const distance = Math.max(0, finite(reach, 0));\n  const end = distance * getTrimBurstEndProgress(time);\n  const own = distance * getTrimBurstStartProgress(time);\n  const length = getTrimBurstLeash(end - own, strokeLength);\n  return { start: end - length, end, length };\n}\n\nexport function getTrimBurstDotScale(time: number) {\n  if (Number.isNaN(time) || time <= DOT_START || time >= LIFE) return 0;\n  const pop = Math.min(1, (time - DOT_START) / DOT_POP) - 1;\n  const grow = 1 + (DOT_BACK + 1) * pop ** 3 + DOT_BACK * pop ** 2;\n  const settle = DOT_START + DOT_POP;\n  const shrink = clamp((time - settle) / (LIFE - settle), 0, 1);\n  return grow * (1 - shrink ** 3);\n}\n\nexport function getTrimBurstStrokes(\n  options: Pick<\n    TrimBurstOptions,\n    \"count\" | \"rotation\" | \"spread\" | \"jitter\" | \"seed\"\n  > = {},\n): TrimBurstStroke[] {\n  const count = countOf(options.count);\n  const rotation = finite(options.rotation, trimBurstDefaults.rotation);\n  const spread = within(options.spread, trimBurstDefaults.spread, 0, 360);\n  const jitter = jitterOf(options.jitter);\n  const seed = finite(options.seed, trimBurstDefaults.seed);\n  const fan = spread < 360 && count > 1;\n  const step = fan ? spread / (count - 1) : 360 / count;\n  const first = fan ? rotation - spread / 2 : rotation;\n  const wobble = WOBBLE * jitter * Math.min(step, MAX_STEP);\n  const { stagger } = getTrimBurstTimeline({ count, jitter });\n  const keys = Array.from({ length: count }, (_, index) =>\n    random(`trim-burst-${seed}-order-${index}`),\n  );\n  const order = keys.map((_, index) => index);\n  order.sort((a, b) => keys[a] - keys[b]);\n  const ranks: number[] = [];\n  for (const [position, index] of order.entries()) ranks[index] = position;\n  return keys.map((_, index) => {\n    const turn = random(`trim-burst-${seed}-angle-${index}`) * 2 - 1;\n    const shrink = random(`trim-burst-${seed}-scale-${index}`);\n    return {\n      index,\n      angle: first + index * step + turn * wobble,\n      scale: 1 - SHRINK * jitter * shrink,\n      launch: count > 1 ? (stagger * ranks[index]) / (count - 1) : 0,\n    };\n  });\n}\n\nexport function getTrimBurstState(\n  frame: number,\n  options: TrimBurstOptions = {},\n  strokes: TrimBurstStroke[] = getTrimBurstStrokes(options),\n) {\n  const fallback = trimBurstDefaults;\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 time = getTrimBurstTime(frame, options);\n  const originX = finite(options.x, fallback.x) * width;\n  const originY = finite(options.y, fallback.y) * height;\n  const inner = within(options.innerRadius, fallback.innerRadius, 0, MAX_SIZE);\n  const reach = within(options.reach, fallback.reach, 0, MAX_SIZE);\n  const strokeLength = within(\n    options.strokeLength,\n    fallback.strokeLength,\n    MIN_STROKE,\n    MAX_SIZE,\n  );\n  const size = within(options.weight, fallback.weight, MIN_WEIGHT, MAX_WEIGHT);\n  const offset = inner * unit;\n  const weight = size * unit;\n  const gap = DOT_GAP * weight;\n  const radius = DOT_RADIUS * weight;\n  const lines: TrimBurstLine[] = [];\n  const dots: TrimBurstDot[] = [];\n  for (const stroke of strokes) {\n    const local = time - stroke.launch;\n    const trim = getTrimBurstTrim(\n      local,\n      reach * unit * stroke.scale,\n      strokeLength * unit * stroke.scale,\n    );\n    const angle = (stroke.angle * Math.PI) / 180;\n    const dx = Math.sin(angle);\n    const dy = -Math.cos(angle);\n    const along = (distance: number) => ({\n      x: round(originX + dx * (offset + distance)),\n      y: round(originY + dy * (offset + distance)),\n    });\n    if (trim.length > VISIBLE) {\n      const from = along(trim.start);\n      const to = along(trim.end);\n      lines.push({\n        index: stroke.index,\n        x1: from.x,\n        y1: from.y,\n        x2: to.x,\n        y2: to.y,\n        width: round(Math.min(weight, trim.length)),\n      });\n    }\n    const pop = options.dots === true ? getTrimBurstDotScale(local) : 0;\n    if (pop > 0) {\n      const center = along(trim.end + gap + radius);\n      dots.push({\n        index: stroke.index,\n        cx: center.x,\n        cy: center.y,\n        r: round(radius * pop),\n      });\n    }\n  }\n  return {\n    width,\n    height,\n    unit,\n    time,\n    originX,\n    originY,\n    weight,\n    cap: capOf(options.cap),\n    lines,\n    dots,\n  };\n}\n\nexport type TrimBurstState = ReturnType<typeof getTrimBurstState>;\n\nexport function TrimBurst({\n  x = trimBurstDefaults.x,\n  y = trimBurstDefaults.y,\n  count = trimBurstDefaults.count,\n  innerRadius = trimBurstDefaults.innerRadius,\n  reach = trimBurstDefaults.reach,\n  strokeLength = trimBurstDefaults.strokeLength,\n  weight = trimBurstDefaults.weight,\n  rotation = trimBurstDefaults.rotation,\n  spread = trimBurstDefaults.spread,\n  jitter = trimBurstDefaults.jitter,\n  dots = trimBurstDefaults.dots,\n  cap = trimBurstDefaults.cap,\n  color = trimBurstDefaults.color,\n  seed = trimBurstDefaults.seed,\n  delay = trimBurstDefaults.delay,\n  speed = trimBurstDefaults.speed,\n  className,\n}: TrimBurstProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const strokes = useMemo(\n    () => getTrimBurstStrokes({ count, rotation, spread, jitter, seed }),\n    [count, rotation, spread, jitter, seed],\n  );\n  const state = getTrimBurstState(\n    frame,\n    {\n      width,\n      height,\n      fps,\n      x,\n      y,\n      innerRadius,\n      reach,\n      strokeLength,\n      weight,\n      dots,\n      cap,\n      delay,\n      speed,\n    },\n    strokes,\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=\"Trim burst\"\n        style={{ display: \"block\", overflow: \"visible\" }}\n      >\n        <g fill=\"none\" stroke={color} strokeLinecap={state.cap}>\n          {state.lines.map((line) => (\n            <line\n              key={line.index}\n              x1={line.x1}\n              y1={line.y1}\n              x2={line.x2}\n              y2={line.y2}\n              strokeWidth={line.width}\n            />\n          ))}\n        </g>\n        <g fill={color}>\n          {state.dots.map((dot) => (\n            <circle key={dot.index} cx={dot.cx} cy={dot.cy} r={dot.r} />\n          ))}\n        </g>\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/trim-burst.tsx"
    }
  ],
  "type": "registry:component"
}
