{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flow-arrow",
  "title": "Flow Arrow",
  "description": "A dotted gradient arrow zigzags upward beside a 3x growth stat.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/flow-arrow/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSpace } from \"@remotion/google-fonts/SpaceGrotesk\";\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: space } = loadSpace(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface FlowArrowProps {\n  metric?: string;\n  label?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst FLIGHT = Easing.bezier(0.38, 0, 0.16, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    stream: \"#7C7B82\",\n    hair: \"rgba(255,255,255,0.14)\",\n    shine: \"#FFFFFF\",\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    stream: \"#9C9A94\",\n    hair: \"rgba(18,18,20,0.16)\",\n    shine: \"#0E1240\",\n    grain: 0.045,\n  },\n} as const;\n\nconst TREND: [number, number][] = [\n  [0, 0.02],\n  [0.15, 0.13],\n  [0.26, 0.09],\n  [0.42, 0.3],\n  [0.52, 0.25],\n  [0.69, 0.55],\n  [0.78, 0.5],\n  [1, 0.9],\n];\n\nconst LAUNCH = 4;\nconst SPAN = 52;\nconst JITTER = 8;\nconst FLY = 32;\nconst HIT = LAUNCH + SPAN + JITTER + FLY;\nconst PASSERS = 80;\n\nfunction hash(n: number) {\n  const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return x - Math.floor(x);\n}\n\n/** gradients.sh \"halo\" palette, run from the arrow's tail to its tip. */\nconst HALO = {\n  dark: [\"#3D4DFF\", \"#E2457A\", \"#FFB36B\", \"#FFF4EC\"],\n  light: [\"#3D4DFF\", \"#E2457A\", \"#F2894A\"],\n} as const;\n\nfunction sample(stops: readonly string[], k: number) {\n  const x = Math.min(1, Math.max(0, k)) * (stops.length - 1);\n  const i = Math.min(stops.length - 2, Math.floor(x));\n  return mix(stops[i], stops[i + 1], x - i);\n}\n\nfunction mix(a: string, b: string, t: number) {\n  const pa = Number.parseInt(a.slice(1), 16);\n  const pb = Number.parseInt(b.slice(1), 16);\n  const ch = (p: number, s: number) => (p >> s) & 255;\n  const c = (s: number) =>\n    Math.round(ch(pa, s) + (ch(pb, s) - ch(pa, s)) * t)\n      .toString(16)\n      .padStart(2, \"0\");\n  return `#${c(16)}${c(8)}${c(0)}`;\n}\n\ntype Pt = { x: number; y: number };\n\nfunction catmull(pts: Pt[], steps: number) {\n  const out: Pt[] = [];\n  for (let i = 0; i < pts.length - 1; i++) {\n    const p0 = pts[Math.max(0, i - 1)];\n    const p1 = pts[i];\n    const p2 = pts[i + 1];\n    const p3 = pts[Math.min(pts.length - 1, i + 2)];\n    for (let s = 0; s < steps; s++) {\n      const t = s / steps;\n      const t2 = t * t;\n      const t3 = t2 * t;\n      const f = (a: number, b: number, c: number, d: number) =>\n        0.5 *\n        (2 * b +\n          (-a + c) * t +\n          (2 * a - 5 * b + 4 * c - d) * t2 +\n          (-a + 3 * b - 3 * c + d) * t3);\n      out.push({ x: f(p0.x, p1.x, p2.x, p3.x), y: f(p0.y, p1.y, p2.y, p3.y) });\n    }\n  }\n  out.push(pts[pts.length - 1]);\n  return out;\n}\n\nfunction cubic(a: Pt, b: Pt, c: Pt, d: Pt, t: number): Pt {\n  const q = 1 - t;\n  const w0 = q * q * q;\n  const w1 = 3 * q * q * t;\n  const w2 = 3 * q * t * t;\n  const w3 = t * t * t;\n  return {\n    x: w0 * a.x + w1 * b.x + w2 * c.x + w3 * d.x,\n    y: w0 * a.y + w1 * b.y + w2 * c.y + w3 * d.y,\n  };\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={23}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nfunction buildArrow(W: number, H: number, u: number) {\n  const bx0 = W * 0.13;\n  const bw = W * 0.5;\n  const by1 = H * 0.78;\n  const bh = H * 0.54;\n  const knots = TREND.map(([x, y]) => ({ x: bx0 + x * bw, y: by1 - y * bh }));\n  const line = catmull(knots, 26);\n  const cum = [0];\n  for (let i = 1; i < line.length; i++) {\n    cum.push(\n      cum[i - 1] +\n        Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y),\n    );\n  }\n  const L = cum[cum.length - 1];\n  const end = line[line.length - 1];\n  const prev = line[line.length - 10];\n  const dl = Math.hypot(end.x - prev.x, end.y - prev.y) || 1;\n  const d = { x: (end.x - prev.x) / dl, y: (end.y - prev.y) / dl };\n  const n = { x: -d.y, y: d.x };\n  const half = 17 * u;\n  const hw = 52 * u;\n  const hl = 92 * u;\n  const tip = { x: end.x + d.x * hl, y: end.y + d.y * hl };\n  const total = L + hl;\n\n  const pitch = 9 * u;\n  const rowH = pitch * 0.866;\n  const minX = bx0 - half * 2;\n  const maxX = tip.x + hw;\n  const minY = tip.y - hw;\n  const maxY = by1 + half * 2;\n  const pts: { x: number; y: number; s: number }[] = [];\n  for (let gy = minY, row = 0; gy <= maxY; gy += rowH, row++) {\n    const off = row % 2 ? pitch / 2 : 0;\n    for (let gx = minX + off; gx <= maxX; gx += pitch) {\n      const rx = gx - end.x;\n      const ry = gy - end.y;\n      const along = rx * d.x + ry * d.y;\n      const across = rx * n.x + ry * n.y;\n      if (\n        along >= -1 &&\n        along <= hl &&\n        Math.abs(across) <= hw * (1 - along / hl)\n      ) {\n        pts.push({ x: gx, y: gy, s: (L + along) / total });\n        continue;\n      }\n      if (along > -1) continue;\n      let best = Number.POSITIVE_INFINITY;\n      let bestS = 0;\n      for (let i = 0; i < line.length - 1; i++) {\n        const a = line[i];\n        const b = line[i + 1];\n        const vx = b.x - a.x;\n        const vy = b.y - a.y;\n        const len2 = vx * vx + vy * vy || 1;\n        const k = Math.max(\n          0,\n          Math.min(1, ((gx - a.x) * vx + (gy - a.y) * vy) / len2),\n        );\n        const dist = Math.hypot(gx - (a.x + vx * k), gy - (a.y + vy * k));\n        if (dist < best) {\n          best = dist;\n          bestS = cum[i] + (cum[i + 1] - cum[i]) * k;\n        }\n      }\n      if (best <= half) pts.push({ x: gx, y: gy, s: bestS / total });\n    }\n  }\n  return { pts, tip, start: knots[0], by1 };\n}\n\nexport function FlowArrow({\n  metric = \"3×\",\n  label = \"faster growth\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: FlowArrowProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const p = THEMES[theme];\n  // The tail starts in the accent (the palette's own first stop is the same blue).\n  const halo = useMemo(\n    () => [accent, ...HALO[theme].slice(1)],\n    [accent, theme],\n  );\n  const r = 3.1 * u;\n\n  const arrow = useMemo(() => buildArrow(W, H, u), [W, H, u]);\n\n  const particles = useMemo(\n    () =>\n      arrow.pts.map((pt, k) => {\n        const h = (o: number) => hash(k * 2.31 + o);\n        const o = h(1) * 2 - 1;\n        const a = { x: -W * 0.05, y: H * (0.56 + o * 0.05) };\n        const b = {\n          x: a.x + (pt.x - a.x) * 0.42,\n          y: H * (0.26 + o * 0.05) + (pt.y - H * 0.5) * 0.35,\n        };\n        const c = {\n          x: pt.x - W * (0.05 + h(5) * 0.03),\n          y: pt.y - H * (0.16 + h(6) * 0.05),\n        };\n        return {\n          k,\n          s: pt.s,\n          color: sample(halo, pt.s ** 1.15),\n          a,\n          b,\n          c,\n          d: { x: pt.x, y: pt.y },\n          start: LAUNCH + pt.s * SPAN + h(7) * JITTER,\n        };\n      }),\n    [arrow, W, H, halo],\n  );\n\n  const passers = useMemo(\n    () =>\n      Array.from({ length: PASSERS }, (_, k) => {\n        const h = (o: number) => hash(k * 5.17 + 40 + o);\n        const o = h(1) * 2 - 1;\n        return {\n          k,\n          a: { x: -W * 0.05, y: H * (0.56 + o * 0.05) },\n          b: { x: W * 0.36, y: H * (0.2 + o * 0.06) },\n          c: { x: W * 0.7, y: H * (0.62 + o * 0.07) },\n          d: { x: W * 1.06, y: H * (0.42 + o * 0.08) },\n          start: h(5) * 54 - 14,\n          dur: 46 + h(6) * 12,\n        };\n      }),\n    [W, H],\n  );\n\n  const sweep = interpolate(t, [HIT - 2, HIT + 26], [-0.15, 1.15], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const land = interpolate(t, [HIT - 6, HIT + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const labIn = interpolate(t, [HIT + 4, HIT + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const hairIn = interpolate(t, [6, 60], [0, 1], { ...CLAMP, easing: EASE_IO });\n  const push = interpolate(t, [0, HIT + 50], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const scale = 0.975 + 0.035 * push;\n\n  const marks: React.ReactNode[] = [];\n  for (const q of passers) {\n    const raw = (t - q.start) / q.dur;\n    if (raw <= 0 || raw >= 1) continue;\n    const e = EASE_IO(raw);\n    const head = cubic(q.a, q.b, q.c, q.d, e);\n    const tail = cubic(q.a, q.b, q.c, q.d, Math.max(0, e - 0.025));\n    const fade = Math.min(1, Math.sin(Math.PI * raw) * 2);\n    marks.push(\n      <line\n        key={`p${q.k}`}\n        x1={tail.x}\n        y1={tail.y}\n        x2={head.x}\n        y2={head.y}\n        stroke={p.stream}\n        strokeWidth={r * 1.6}\n        strokeLinecap=\"round\"\n        opacity={0.5 * fade}\n      />,\n    );\n  }\n  for (const q of particles) {\n    const raw = interpolate(t, [q.start, q.start + FLY], [0, 1], CLAMP);\n    if (raw <= 0) continue;\n    if (raw >= 1) {\n      const g = Math.exp(-(((q.s - sweep) / 0.07) ** 2));\n      const settle = interpolate(\n        t,\n        [q.start + FLY, q.start + FLY + 6],\n        [1.3, 1],\n        CLAMP,\n      );\n      marks.push(\n        <circle\n          key={q.k}\n          cx={q.d.x}\n          cy={q.d.y}\n          r={r * settle}\n          fill={g > 0.02 ? mix(q.color, p.shine, 0.45 * g) : q.color}\n        />,\n      );\n      continue;\n    }\n    const e = FLIGHT(raw);\n    const head = cubic(q.a, q.b, q.c, q.d, e);\n    const tail = cubic(q.a, q.b, q.c, q.d, Math.max(0, e - 0.022));\n    const tone = interpolate(raw, [0.35, 0.95], [0, 1], CLAMP);\n    marks.push(\n      <line\n        key={q.k}\n        x1={tail.x}\n        y1={tail.y}\n        x2={head.x}\n        y2={head.y}\n        stroke={mix(p.stream, q.color, tone)}\n        strokeWidth={r * 1.7}\n        strokeLinecap=\"round\"\n        opacity={interpolate(raw, [0, 0.15], [0.3, 0.95], CLAMP)}\n      />,\n    );\n  }\n\n  const baseY = arrow.by1 + 34 * u;\n  const baseX0 = arrow.start.x - 10 * u;\n  const baseX1 = W * 0.9;\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{ transform: `scale(${scale})`, transformOrigin: \"50% 50%\" }}\n      >\n        <svg\n          width={W}\n          height={H}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <line\n            x1={baseX0}\n            y1={baseY}\n            x2={baseX0 + (baseX1 - baseX0) * hairIn}\n            y2={baseY}\n            stroke={p.hair}\n            strokeWidth={Math.max(1, u)}\n          />\n          {marks}\n        </svg>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: arrow.tip.x + 44 * u,\n            top: arrow.tip.y - 30 * u,\n            display: \"flex\",\n            flexDirection: \"column\",\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: space,\n              fontWeight: 400,\n              fontSize: 176 * u,\n              lineHeight: 0.9,\n              letterSpacing: \"-0.04em\",\n              color: p.fg,\n              fontVariantNumeric: \"tabular-nums\",\n              opacity: land,\n              filter: land < 0.99 ? `blur(${(1 - land) * 8 * u}px)` : undefined,\n              transform: `translate(${(1 - land) * -16 * u}px, ${(1 - land) * 14 * u}px)`,\n            }}\n          >\n            {metric}\n          </div>\n          <div\n            style={{\n              marginTop: 16 * u,\n              fontFamily: space,\n              fontWeight: 400,\n              fontSize: 26 * u,\n              letterSpacing: \"-0.01em\",\n              color: p.muted,\n              opacity: labIn,\n              transform: `translateY(${(1 - labIn) * 8 * u}px)`,\n            }}\n          >\n            {label}\n          </div>\n        </div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 80% 80% at 50% 50%, transparent 62%, ${p.bg} 100%)`,\n          opacity: 0.6,\n          pointerEvents: \"none\",\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/flow-arrow.tsx"
    }
  ],
  "type": "registry:component"
}
