{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread-weave",
  "title": "Thread Weave",
  "description": "A number is woven thread by thread on a loom of cream cells in blue and orange.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/thread-weave/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadPlexMono } from \"@remotion/google-fonts/IBMPlexMono\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadSpace } from \"@remotion/google-fonts/SpaceGrotesk\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadSpace(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadPlexMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\n\nexport interface ThreadWeaveProps {\n  value?: string;\n  motif?: \"value\" | \"mark\";\n  caption?: string;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\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 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    weft: \"#ECE9E2\",\n    warp: \"#B9B4AA\",\n    shade: \"rgba(10,10,11,0.28)\",\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    weft: \"#1E1E21\",\n    warp: \"#6A6A71\",\n    shade: \"rgba(0,0,0,0.45)\",\n    grain: 0.045,\n  },\n} as const;\n\nconst FONT: Record<string, string[]> = {\n  \"0\": [\".###.\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \"#...#\", \".###.\"],\n  \"1\": [\"..#..\", \".##..\", \"..#..\", \"..#..\", \"..#..\", \"..#..\", \".###.\"],\n  \"2\": [\".###.\", \"#...#\", \"....#\", \"...#.\", \"..#..\", \".#...\", \"#####\"],\n  \"3\": [\"####.\", \"....#\", \"....#\", \".###.\", \"....#\", \"....#\", \"####.\"],\n  \"4\": [\"...#.\", \"..##.\", \".#.#.\", \"#..#.\", \"#####\", \"...#.\", \"...#.\"],\n  \"5\": [\"#####\", \"#....\", \"####.\", \"....#\", \"....#\", \"#...#\", \".###.\"],\n  \"6\": [\"..##.\", \".#...\", \"#....\", \"####.\", \"#...#\", \"#...#\", \".###.\"],\n  \"7\": [\"#####\", \"....#\", \"...#.\", \"..#..\", \".#...\", \".#...\", \".#...\"],\n  \"8\": [\".###.\", \"#...#\", \"#...#\", \".###.\", \"#...#\", \"#...#\", \".###.\"],\n  \"9\": [\".###.\", \"#...#\", \"#...#\", \".####\", \"....#\", \"...#.\", \".##..\"],\n  \"+\": [\".....\", \"..#..\", \"..#..\", \"#####\", \"..#..\", \"..#..\", \".....\"],\n  \"%\": [\"##..#\", \"##.#.\", \"...#.\", \"..#..\", \".#...\", \".#.##\", \"#..##\"],\n  x: [\".....\", \".....\", \"#...#\", \".#.#.\", \"..#..\", \".#.#.\", \"#...#\"],\n  k: [\"#....\", \"#....\", \"#..#.\", \"#.#..\", \"##...\", \"#.#..\", \"#..#.\"],\n  \".\": [\".\", \".\", \".\", \".\", \".\", \".\", \"#\"],\n  \" \": [\"..\", \"..\", \"..\", \"..\", \"..\", \"..\", \"..\"],\n};\n\n/** The remocn.dev R mark, rasterised from its outline (13 × 12, ≥42% coverage). */\nconst MARK = [\n  \"1111111111..\",\n  \".1111111111.\",\n  \".........11.\",\n  \"..........11\",\n  \"..........11\",\n  \".........111\",\n  \".....111111.\",\n  \".111111111..\",\n  \"11111111....\",\n  \"11....111...\",\n  \"11.....11...\",\n  \"11......11..\",\n  \"11.......11.\",\n];\n\n/**\n * gradients.sh \"fluted\" palette (#1d2a8f #2f7bff #ff7a3d #ffe2c4; the\n * accent stands in for #2f7bff on light cloth) dyes the figure threads,\n * bottom-left to top-right. Light cloth takes the dark end, dark cloth the\n * bright end.\n */\nfunction figureColor(theme: \"dark\" | \"light\", accent: string, k: number) {\n  return theme === \"dark\"\n    ? interpolateColors(k, [0, 0.45, 1], [\"#1d2a8f\", accent, \"#ff7a3d\"])\n    : interpolateColors(k, [0, 0.5, 1], [\"#2f7bff\", \"#ff7a3d\", \"#ffe2c4\"]);\n}\n\n/** Longest the weave may take, so dense motifs still hold their poster. */\nconst WEAVE_BUDGET = 108;\n\nfunction motifBitmap(value: string, motif: \"value\" | \"mark\") {\n  if (motif === \"mark\") return MARK;\n  const rows: string[] = [\"\", \"\", \"\", \"\", \"\", \"\", \"\"];\n  const chars = value.split(\"\").filter((ch) => FONT[ch]);\n  chars.forEach((ch, i) => {\n    const g = FONT[ch];\n    for (let r = 0; r < 7; r++) {\n      rows[r] += g[r].replace(/#/g, \"1\") + (i < chars.length - 1 ? \".\" : \"\");\n    }\n  });\n  return rows;\n}\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** remocn.dev logo: R mark (0.76em, on the baseline) + \"emocn\" in Manrope 600. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\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 }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={7}\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\nconst lerp = (a: number, b: number, k: number) => a + (b - a) * k;\n\nconst WARP_IN = [-6, 22] as const;\nconst WEAVE_START = 22;\nconst PAD = { value: [3, 2], mark: [2, 1] } as const;\n\nexport function ThreadWeave({\n  value = \"24\",\n  motif = \"value\",\n  caption = \"frames per second\",\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: ThreadWeaveProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const u = height / 720;\n  const p = THEMES[theme];\n\n  const bitmap = motifBitmap(value, motif);\n  const [PAD_X, PAD_Y] = PAD[motif];\n  const bw = Math.max(...bitmap.map((r) => r.length), 1);\n  const bh = bitmap.length;\n  const pxW = bw + PAD_X * 2;\n  const pxH = bh + PAD_Y * 2;\n  const cols = pxW * 2;\n  const rows = pxH * 2;\n\n  const sy = Math.min((height * 0.6) / rows, (width * 0.7) / (cols * 0.94));\n  const sx = sy * 0.94;\n  const clothW = cols * sx;\n  const clothH = rows * sy;\n  const footer = 46 * u;\n  const left = (width - clothW) / 2;\n  const top = (height - clothH - footer) / 2;\n  const fringe = 0.9 * sy;\n  const barH = sy * 0.86;\n  const warpW = sx * 0.3;\n\n  const colorAt = (r: number, c: number) => {\n    const py = Math.floor(r / 2) - PAD_Y;\n    const px = Math.floor(c / 2) - PAD_X;\n    const ch = bitmap[py]?.[px];\n    if (ch !== \"1\") return p.weft;\n    const gx = px / Math.max(1, bw - 1);\n    const gy = py / Math.max(1, bh - 1);\n    return figureColor(theme, accent, 0.55 * gx + 0.45 * (1 - gy));\n  };\n\n  const durs: number[] = [];\n  const starts: number[] = [];\n  const raw = Array.from({ length: rows }, (_, r) =>\n    lerp(6.6, 3.4, EASE_OUT(r / Math.max(1, rows - 1))),\n  );\n  const fit = Math.min(1, WEAVE_BUDGET / raw.reduce((a, b) => a + b, 0));\n  let acc = WEAVE_START;\n  for (let r = 0; r < rows; r++) {\n    const d = raw[r] * fit;\n    starts.push(acc);\n    durs.push(d);\n    acc += d;\n  }\n  const weaveEnd = acc;\n  const beatLen = 7;\n\n  const rowState = (r: number) => {\n    const s = starts[r];\n    const e = s + durs[r];\n    const prog = interpolate(t, [s, e], [0, 1], { ...CLAMP, easing: EASE_IO });\n    const beat = interpolate(t, [e, e + beatLen], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const y = top + (r + 0.5) * sy - (1 - beat) * 0.32 * sy;\n    const ltr = r % 2 === 0;\n    return { prog, y, ltr, started: t >= s };\n  };\n\n  const states = Array.from({ length: rows }, (_, r) => rowState(r));\n  const picks = states.filter((s) => s.prog >= 1).length;\n\n  const settle = interpolate(t, [weaveEnd, weaveEnd + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const footA = interpolate(t, [10, 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const capA = interpolate(t, [weaveEnd + 2, weaveEnd + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const warpEls = Array.from({ length: cols }, (_, c) => {\n    const x = left + (c + 0.5) * sx;\n    const mid = (cols - 1) / 2;\n    const delay = (Math.abs(c - mid) / Math.max(1, mid)) * 10;\n    const g = interpolate(\n      t,\n      [WARP_IN[0] + delay, WARP_IN[1] - 10 + delay],\n      [0, 1],\n      {\n        ...CLAMP,\n        easing: EASE_OUT,\n      },\n    );\n    const y0 = top - fringe;\n    const y1 = top + clothH + fringe;\n    return (\n      <rect\n        key={`w${x}`}\n        x={x - warpW / 2}\n        y={y0}\n        width={warpW}\n        height={(y1 - y0) * g}\n        rx={warpW / 2}\n        fill={p.warp}\n      />\n    );\n  });\n\n  const weftEls: React.ReactNode[] = [];\n  const tickEls: React.ReactNode[] = [];\n  const loopEls: React.ReactNode[] = [];\n  const xL = left;\n  const xR = left + clothW;\n  const capR = barH / 2;\n\n  for (let r = 0; r < rows; r++) {\n    const st = states[r];\n    if (!st.started) continue;\n    const reach = st.prog;\n    const tip = st.ltr ? lerp(xL, xR, reach) : lerp(xR, xL, reach);\n    const visLo = st.ltr ? xL : tip;\n    const visHi = st.ltr ? tip : xR;\n    for (let c = 0; c < cols; c++) {\n      const cx0 = left + c * sx;\n      const cx1 = cx0 + sx;\n      const lo = Math.max(cx0, visLo);\n      const hi = Math.min(cx1, visHi);\n      if (hi <= lo) continue;\n      weftEls.push(\n        <rect\n          key={`f${r}-${c}`}\n          x={lo - 0.4}\n          y={st.y - barH / 2}\n          width={hi - lo + 0.8}\n          height={barH}\n          fill={colorAt(r, c)}\n        />,\n      );\n      const wx = cx0 + sx / 2;\n      const passed = st.ltr ? tip >= wx + warpW : tip <= wx - warpW;\n      if ((r + c) % 2 === 1 && passed) {\n        tickEls.push(\n          <g key={`t${r}-${c}`}>\n            <rect\n              x={wx - warpW / 2 - 1.6 * u}\n              y={st.y - barH / 2}\n              width={warpW + 3.2 * u}\n              height={barH}\n              fill={p.shade}\n            />\n            <rect\n              x={wx - warpW / 2}\n              y={st.y - barH / 2 - 0.5}\n              width={warpW}\n              height={barH + 1}\n              fill={p.warp}\n            />\n          </g>,\n        );\n      }\n    }\n    const lead = st.prog > 0 && st.prog < 1 ? tip : null;\n    if (lead !== null) {\n      weftEls.push(\n        <circle\n          key={`h${r}`}\n          cx={lead}\n          cy={st.y}\n          r={capR}\n          fill={colorAt(\n            r,\n            Math.max(0, Math.min(cols - 1, Math.floor((lead - left) / sx))),\n          )}\n        />,\n      );\n    }\n    if (r === 0 && st.prog > 0) {\n      weftEls.push(\n        <circle key=\"cap0\" cx={xL} cy={st.y} r={capR} fill={p.weft} />,\n      );\n    }\n    if (r === rows - 1 && st.prog >= 1) {\n      weftEls.push(\n        <circle\n          key=\"cap1\"\n          cx={st.ltr ? xR : xL}\n          cy={st.y}\n          r={capR}\n          fill={p.weft}\n        />,\n      );\n    }\n    if (r > 0) {\n      const prev = states[r - 1];\n      const side = prev.ltr ? xR : xL;\n      const dir = prev.ltr ? 1 : -1;\n      const k = interpolate(st.prog, [0, 0.12], [0, 1], CLAMP);\n      if (k > 0) {\n        const rr = (st.y - prev.y) / 2;\n        const rx = sy * 0.3;\n        const cyM = (st.y + prev.y) / 2;\n        const ang = Math.PI * k;\n        const ex = side + dir * Math.sin(ang) * rx;\n        const ey = cyM - Math.cos(ang) * rr;\n        loopEls.push(\n          <path\n            key={`l${r}`}\n            d={`M${side} ${prev.y} A${rx} ${rr} 0 0 ${dir > 0 ? 1 : 0} ${ex} ${ey}`}\n            fill=\"none\"\n            stroke={p.weft}\n            strokeWidth={barH * 0.9}\n          />,\n        );\n      }\n    }\n  }\n\n  const scale = lerp(1.025, 1, EASE_IO(Math.min(1, t / (weaveEnd + 16))));\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `scale(${scale})`,\n          transformOrigin: \"50% 46%\",\n        }}\n      >\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          {warpEls}\n          {loopEls}\n          {weftEls}\n          {tickEls}\n        </svg>\n        <div\n          style={{\n            position: \"absolute\",\n            left,\n            top: top + clothH + fringe + 18 * u,\n            width: clothW,\n            display: \"flex\",\n            justifyContent: \"space-between\",\n            alignItems: \"baseline\",\n            fontFamily: sans,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          <div\n            style={{\n              fontSize: 20 * u,\n              fontWeight: 500,\n              letterSpacing: \"-0.015em\",\n              color: p.fg,\n              opacity: capA,\n              transform: `translateY(${(1 - capA) * 8 * u}px)`,\n            }}\n          >\n            {caption === \"remocn\" ? (\n              <RemocnLogo fontSize={22 * u} color={p.fg} />\n            ) : (\n              caption\n            )}\n          </div>\n          <div\n            style={{\n              fontFamily: mono,\n              fontSize: 14 * u,\n              fontWeight: 400,\n              letterSpacing: \"0.01em\",\n              color: p.muted,\n              fontVariantNumeric: \"tabular-nums\",\n              opacity: footA * lerp(1, 0.85, settle),\n            }}\n          >\n            {`${cols} ends × ${picks} picks`}\n          </div>\n        </div>\n      </AbsoluteFill>\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/thread-weave.tsx"
    }
  ],
  "type": "registry:component"
}
