{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-type",
  "title": "Dot Type",
  "description": "Words are drawn from a field of round dots in a gradient, the last one in accent.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/dot-type/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/Sora\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst DISPLAY = loadDisplay(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface DotTypeProps {\n  words?: string[];\n  accentLast?: boolean;\n  count?: number;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst DEFAULT_WORDS = [\"Think.\", \"Make.\", \"Ship.\"];\n\nconst FIELD = 14;\nconst FIRST = 32;\nconst STREAM = 34;\nconst HOLD = 22;\nconst FINAL_HOLD = 32;\nconst FLIGHT = 0.66;\n\nconst INOUT = Easing.bezier(0.65, 0, 0.35, 1);\nconst SOFT = Easing.bezier(0.45, 0, 0.2, 1);\n\nfunction windows(wordCount: number) {\n  const out: { start: number; len: number }[] = [];\n  let t = FIELD;\n  for (let i = 0; i < wordCount; i++) {\n    const len = i === 0 ? FIRST : STREAM;\n    out.push({ start: t, len });\n    t += len + HOLD;\n  }\n  return out;\n}\n\nexport const dotTypeLength = (wordCount = DEFAULT_WORDS.length) => {\n  const w = windows(wordCount);\n  const last = w[w.length - 1];\n  return last.start + last.len + FINAL_HOLD;\n};\n\nfunction hash(n: number): number {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nfunction valueNoise(x: number, y: number): number {\n  const xi = Math.floor(x);\n  const yi = Math.floor(y);\n  const xf = x - xi;\n  const yf = y - yi;\n  const h = (a: number, b: number) => hash(a * 57.3 + b * 131.9);\n  const u = xf * xf * (3 - 2 * xf);\n  const v = yf * yf * (3 - 2 * yf);\n  const top = h(xi, yi) * (1 - u) + h(xi + 1, yi) * u;\n  const bot = h(xi, yi + 1) * (1 - u) + h(xi + 1, yi + 1) * u;\n  return top * (1 - v) + bot * v;\n}\n\ninterface Pt {\n  x: number;\n  y: number;\n}\n\ninterface WordSample {\n  pts: Pt[];\n  spacing: number;\n  cy: number;\n}\n\nfunction hilbert(x: number, y: number, order = 128): number {\n  let d = 0;\n  let px = x;\n  let py = y;\n  for (let s = order / 2; s >= 1; s = Math.floor(s / 2)) {\n    const rx = (px & s) > 0 ? 1 : 0;\n    const ry = (py & s) > 0 ? 1 : 0;\n    d += s * s * ((3 * rx) ^ ry);\n    if (ry === 0) {\n      if (rx === 1) {\n        px = s - 1 - px;\n        py = s - 1 - py;\n      }\n      const t = px;\n      px = py;\n      py = t;\n    }\n  }\n  return d;\n}\n\nfunction hilbertOrder(pts: Pt[]): number[] {\n  let minX = Number.POSITIVE_INFINITY;\n  let minY = Number.POSITIVE_INFINITY;\n  let maxX = Number.NEGATIVE_INFINITY;\n  let maxY = Number.NEGATIVE_INFINITY;\n  for (const p of pts) {\n    minX = Math.min(minX, p.x);\n    minY = Math.min(minY, p.y);\n    maxX = Math.max(maxX, p.x);\n    maxY = Math.max(maxY, p.y);\n  }\n  const span = Math.max(maxX - minX, maxY - minY) || 1;\n  const ox = minX - (span - (maxX - minX)) / 2;\n  const oy = minY - (span - (maxY - minY)) / 2;\n  return pts\n    .map((p, i) => ({\n      i,\n      h: hilbert(\n        Math.min(127, Math.floor(((p.x - ox) / span) * 128)),\n        Math.min(127, Math.floor(((p.y - oy) / span) * 128)),\n      ),\n    }))\n    .sort((m, n) => m.h - n.h)\n    .map((m) => m.i);\n}\n\nfunction xOrder(pts: Pt[]): number[] {\n  return pts\n    .map((p, i) => ({ i, k: p.x + p.y * 1e-4 }))\n    .sort((m, n) => m.k - n.k)\n    .map((m) => m.i);\n}\n\nfunction sampleWords(\n  words: string[],\n  family: string,\n  n: number,\n  W: number,\n  H: number,\n): WordSample[] | null {\n  if (typeof document === \"undefined\") return null;\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = W;\n  canvas.height = H;\n  const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n  if (!ctx) return null;\n  const setFont = (px: number) => {\n    ctx.font = `500 ${px}px \"${family}\", sans-serif`;\n    if (\"letterSpacing\" in ctx) {\n      (\n        ctx as CanvasRenderingContext2D & { letterSpacing: string }\n      ).letterSpacing = `${(-0.03 * px).toFixed(2)}px`;\n    }\n  };\n  setFont(100);\n  let widest = 1;\n  for (const w of words) widest = Math.max(widest, ctx.measureText(w).width);\n  const cap = ctx.measureText(\"H\").actualBoundingBoxAscent || 72;\n  const fs = Math.min((W * 0.66 * 100) / widest, (H * 0.34 * 100) / cap);\n  setFont(fs);\n  const capPx = ctx.measureText(\"H\").actualBoundingBoxAscent || fs * 0.72;\n  const baseline = H / 2 + capPx / 2;\n  ctx.textAlign = \"center\";\n  ctx.textBaseline = \"alphabetic\";\n\n  return words.map((word) => {\n    ctx.clearRect(0, 0, W, H);\n    ctx.fillStyle = \"#fff\";\n    ctx.fillText(word, W / 2, baseline);\n    const data = ctx.getImageData(0, 0, W, H).data;\n    let x0 = W;\n    let y0 = H;\n    let x1 = 0;\n    let y1 = 0;\n    for (let y = 0; y < H; y += 2) {\n      for (let x = 0; x < W; x += 2) {\n        if (data[(y * W + x) * 4 + 3] > 127) {\n          if (x < x0) x0 = x;\n          if (x > x1) x1 = x;\n          if (y < y0) y0 = y;\n          if (y > y1) y1 = y;\n        }\n      }\n    }\n    x0 = Math.max(0, x0 - 4);\n    y0 = Math.max(0, y0 - 4);\n    x1 = Math.min(W - 1, x1 + 4);\n    y1 = Math.min(H - 1, y1 + 4);\n    const grid = (s: number) => {\n      const out: Pt[] = [];\n      const rowH = s * 0.866;\n      let row = 0;\n      for (let y = y0 + rowH / 2; y < y1; y += rowH, row++) {\n        const off = row % 2 ? s / 2 : 0;\n        for (let x = x0 + off; x < x1; x += s) {\n          const px = Math.round(x);\n          const py = Math.round(y);\n          if (data[(py * W + px) * 4 + 3] > 127) out.push({ x, y });\n        }\n      }\n      return out;\n    };\n    let lo = 1;\n    let hi = 40;\n    for (let it = 0; it < 16; it++) {\n      const mid = (lo + hi) / 2;\n      if (grid(mid).length >= n) lo = mid;\n      else hi = mid;\n    }\n    const pts = grid(lo);\n    const picked: Pt[] = [];\n    for (let j = 0; j < n; j++) {\n      picked.push(pts[Math.floor(((j + 0.5) * pts.length) / n)] ?? pts[0]);\n    }\n    return { pts: picked, spacing: lo, cy: H / 2 };\n  });\n}\n\nfunction fieldPoints(n: number, W: number, H: number) {\n  const cols = Math.round(Math.sqrt((n * W) / H));\n  const rows = Math.ceil(n / cols);\n  const pts: (Pt & { b: number })[] = [];\n  for (let i = 0; i < n; i++) {\n    const c = i % cols;\n    const r = Math.floor(i / cols);\n    const x = ((c + 0.15 + hash(i * 3.1) * 0.7) / cols) * W;\n    const y = ((r + 0.15 + hash(i * 5.7) * 0.7) / rows) * H;\n    const nx = x / W - 0.5;\n    const ny = y / H - 0.5;\n    const vignette = Math.max(0, 1 - (nx * nx * 2.6 + ny * ny * 3.4));\n    const b = valueNoise(x / (W * 0.14), y / (H * 0.2)) ** 1.4;\n    pts.push({ x, y, b: (0.12 + b * 0.55) * (0.25 + 0.75 * vignette) });\n  }\n  return pts;\n}\n\nfunction Grain({ opacity, light }: { opacity: number; light: boolean }) {\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={`${uid}-grain`}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency={0.85}\n          numOctaves={2}\n          seed={7}\n        />\n        <feColorMatrix\n          type=\"matrix\"\n          values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 1 0\"\n        />\n      </filter>\n      <rect\n        width=\"100%\"\n        height=\"100%\"\n        filter={`url(#${uid}-grain)`}\n        opacity={opacity}\n        style={{ mixBlendMode: light ? \"multiply\" : \"screen\" }}\n      />\n    </svg>\n  );\n}\n\n/* gradients.sh \"dither-field\" palette, as a fill on the last word's dots. */\nconst DITHER = {\n  dark: [\"#2b3dd8\", \"#ff6a3d\", \"#ffd9a8\"],\n  light: [\"#2b3dd8\", \"#ff6a3d\", \"#e0703b\"],\n} as const;\n\nfunction circle(x: number, y: number, r: number) {\n  const rs = r.toFixed(2);\n  const d = (2 * r).toFixed(2);\n  return `M${(x - r).toFixed(1)} ${y.toFixed(1)}a${rs} ${rs} 0 1 0 ${d} 0a${rs} ${rs} 0 1 0 -${d} 0`;\n}\n\nexport function DotType({\n  words = DEFAULT_WORDS,\n  accentLast = true,\n  count = 1500,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: DotTypeProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const isLight = theme === \"light\";\n  const bg = isLight ? \"#F5F4F0\" : \"#0A0A0B\";\n  const ink = isLight ? \"#141416\" : \"#EEEEF0\";\n  const n = Math.max(300, Math.round(count));\n  const list = words.length ? words : DEFAULT_WORDS;\n  const key = list.join(\"\\u0000\");\n\n  const [ready, setReady] = useState(false);\n  const [handle] = useState(() => delayRender(\"dot-type: font\"));\n  const released = useRef(false);\n\n  useEffect(() => {\n    let alive = true;\n    const done = () => {\n      if (alive) setReady(true);\n    };\n    DISPLAY.waitUntilDone()\n      .then(() =>\n        typeof document !== \"undefined\" && document.fonts\n          ? document.fonts.load(`500 120px \"${DISPLAY.fontFamily}\"`)\n          : undefined,\n      )\n      .then(done, done);\n    return () => {\n      alive = false;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (ready && !released.current) {\n      released.current = true;\n      continueRender(handle);\n    }\n  }, [ready, handle]);\n\n  const model = useMemo(() => {\n    if (!ready) return null;\n    const samples = sampleWords(\n      key.split(\"\\u0000\"),\n      DISPLAY.fontFamily,\n      n,\n      W,\n      H,\n    );\n    if (!samples) return null;\n    const field = fieldPoints(n, W, H);\n    const fieldIdx = hilbertOrder(field);\n    const firstIdx = hilbertOrder(samples[0].pts);\n    const states: Pt[][] = [];\n    const s0: Pt[] = new Array(n);\n    const s1: Pt[] = new Array(n);\n    const fieldB: number[] = new Array(n);\n    for (let r = 0; r < n; r++) {\n      const dot = fieldIdx[r];\n      s0[dot] = field[dot];\n      fieldB[dot] = field[dot].b;\n      s1[dot] = samples[0].pts[firstIdx[r]];\n    }\n    states.push(s0, s1);\n    const rankOf: number[][] = [];\n    for (let k = 1; k < samples.length; k++) {\n      const prev = states[k];\n      const prevOrder = xOrder(prev);\n      const rank: number[] = new Array(n);\n      prevOrder.forEach((dot, r) => {\n        rank[dot] = r;\n      });\n      rankOf.push(rank);\n      const target = samples[k].pts;\n      const tOrder = xOrder(target);\n      const next: Pt[] = new Array(n);\n      for (let dot = 0; dot < n; dot++)\n        next[dot] = target[tOrder[n - 1 - rank[dot]]];\n      states.push(next);\n    }\n    return {\n      states,\n      fieldB,\n      rankOf,\n      spacing: samples.map((s) => s.spacing),\n    };\n  }, [ready, key, n, W, H]);\n\n  const wins = windows(list.length);\n  const total = dotTypeLength(list.length);\n  const cx = W / 2;\n  const cy = H / 2;\n\n  const push = 1 + 0.035 * INOUT(Math.min(1, frame / total));\n  const gid = `dt${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const sweep = Math.min(1, frame / total);\n  const stops = DITHER[theme];\n\n  const BUCKETS = 6;\n  const inkPaths: string[][] = Array.from({ length: BUCKETS }, () => []);\n  const accPaths: string[][] = Array.from({ length: BUCKETS }, () => []);\n\n  if (model) {\n    let k = 0;\n    for (let i = 0; i < wins.length; i++) if (frame >= wins[i].start) k = i + 1;\n    const { states, fieldB, rankOf, spacing } = model;\n    const from = states[Math.max(0, k - 1)];\n    const to = states[k];\n    const win = wins[Math.max(0, k - 1)];\n    const u = k === 0 ? 0 : (frame - win.start) / win.len;\n    const lastK = list.length;\n    const unit = Math.min(W, H) / 720;\n    const rField = 1.7 * unit;\n    const rWord = (s: number) => s * 0.37;\n    const maxD = Math.hypot(W, H) / 2;\n\n    for (let i = 0; i < n; i++) {\n      const a = from[i];\n      const b = to[i];\n      let x: number;\n      let y: number;\n      let r: number;\n      let bright: number;\n      let acc = false;\n      if (k === 0) {\n        x = a.x + Math.sin(frame * 0.028 + i) * 2 * unit + frame * 0.35 * unit;\n        y = a.y + Math.cos(frame * 0.022 + i * 1.3) * 2 * unit;\n        r = rField * (0.5 + fieldB[i]);\n        bright = fieldB[i];\n      } else if (k === 1) {\n        const ax =\n          a.x + Math.sin(FIELD * 0.028 + i) * 2 * unit + FIELD * 0.35 * unit;\n        const ay = a.y + Math.cos(FIELD * 0.022 + i * 1.3) * 2 * unit;\n        const d = Math.hypot(ax - cx, ay - cy) / maxD;\n        const delay = Math.min(1, d * 0.8 + hash(i * 9.1) * 0.2) * (1 - FLIGHT);\n        const t = Math.max(0, Math.min(1, (u - delay) / FLIGHT));\n        const e = INOUT(t);\n        const dx = b.x - ax;\n        const dy = b.y - ay;\n        const curl = (0.22 + hash(i * 4.3) * 0.14) * Math.sin(Math.PI * e);\n        x = ax + dx * e - dy * curl;\n        y = ay + dy * e + dx * curl;\n        r = rField * (0.5 + fieldB[i]) * (1 - e) + rWord(spacing[0]) * e;\n        bright = fieldB[i] + (1 - fieldB[i]) * e;\n        acc = accentLast && lastK === 1 && e > 0.92;\n      } else {\n        const rank = rankOf[k - 2][i] / n;\n        const delay =\n          Math.min(1, (1 - rank) * 0.82 + hash(i * 7.7) * 0.18) * (1 - FLIGHT);\n        const t = Math.max(0, Math.min(1, (u - delay) / FLIGHT));\n        const e = SOFT(t);\n        const lift = Math.sin(Math.PI * e);\n        const side = a.y < cy ? -1 : 1;\n        const lane = 0.78 + hash(i * 3.3) * 0.44;\n        const theta = Math.PI * e;\n        const ox = cx + Math.cos(theta) * W * 0.36 * (0.9 + lane * 0.1);\n        const oy = cy + side * Math.sin(theta) * H * 0.29 * lane;\n        x = (a.x + (b.x - a.x) * e) * (1 - lift) + ox * lift;\n        y = (a.y + (b.y - a.y) * e) * (1 - lift) + oy * lift;\n        r =\n          (rWord(spacing[k - 2]) * (1 - e) + rWord(spacing[k - 1]) * e) *\n          (1 - lift * 0.3);\n        bright = 1 - lift * 0.35;\n        acc = accentLast && k === lastK && e > 0.9;\n      }\n      const sx = cx + (x - cx) * push;\n      const sy = cy + (y - cy) * push;\n      if (sx < -10 || sx > W + 10) continue;\n      bright = Math.max(0, Math.min(1, bright));\n      const bucket = Math.min(BUCKETS - 1, Math.floor(bright * BUCKETS));\n      (acc ? accPaths : inkPaths)[bucket].push(circle(sx, sy, r * push));\n    }\n  }\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: isLight\n          ? `radial-gradient(80% 75% at 50% 45%, #FBFAF7 0%, ${bg} 72%)`\n          : `radial-gradient(80% 75% at 50% 45%, #121215 0%, ${bg} 72%)`,\n        overflow: \"hidden\",\n      }}\n    >\n      <svg\n        width={W}\n        height={H}\n        viewBox={`0 0 ${W} ${H}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        <defs>\n          <linearGradient\n            id={gid}\n            gradientUnits=\"userSpaceOnUse\"\n            colorInterpolation=\"linearRGB\"\n            x1={W * (0.2 - 0.04 * sweep)}\n            y1={H * 0.36}\n            x2={W * (0.8 - 0.04 * sweep)}\n            y2={H * 0.64}\n          >\n            <stop offset=\"0\" stopColor={accent} />\n            <stop offset=\"0.18\" stopColor={stops[0]} />\n            <stop offset=\"0.66\" stopColor={stops[1]} />\n            <stop offset=\"1\" stopColor={stops[2]} />\n          </linearGradient>\n        </defs>\n        {inkPaths.map((segs, i) =>\n          segs.length ? (\n            <path\n              key={`i${i}`}\n              d={segs.join(\"\")}\n              fill={ink}\n              opacity={0.22 + ((i + 1) / BUCKETS) * 0.78}\n            />\n          ) : null,\n        )}\n        {accPaths.map((segs, i) =>\n          segs.length ? (\n            <path\n              key={`a${i}`}\n              d={segs.join(\"\")}\n              fill={`url(#${gid})`}\n              opacity={0.5 + ((i + 1) / BUCKETS) * 0.5}\n            />\n          ) : null,\n        )}\n      </svg>\n      <Grain opacity={isLight ? 0.045 : 0.06} light={isLight} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/dot-type.tsx"
    }
  ],
  "type": "registry:component"
}
