{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "single-stroke",
  "title": "Single Stroke",
  "description": "A giant 27 drawn in single strokes sits beside a coral 'Mar' panel and a demo-day line.",
  "dependencies": ["remotion", "@remotion/google-fonts", "@remotion/paths"],
  "files": [
    {
      "path": "registry/remocn/single-stroke/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/Archivo\";\nimport { getLength, getPointAtLength, reversePath } from \"@remotion/paths\";\nimport { useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst archivo = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\n\nexport interface SingleStrokeProps {\n  value?: string;\n  unit?: string;\n  label?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    bg: \"#0E4D4A\",\n    panel: \"#FF6B4A\",\n    panelInk: \"#0E4D4A\",\n    stroke: \"#FFC93C\",\n    hair: \"rgba(244,233,216,0.55)\",\n    tip: \"#F4E9D8\",\n  },\n  light: {\n    bg: \"#F4E9D8\",\n    panel: \"#0E4D4A\",\n    panelInk: \"#F4E9D8\",\n    stroke: \"#FF6B4A\",\n    hair: \"rgba(14,77,74,0.4)\",\n    tip: \"#0E4D4A\",\n  },\n} as const;\n\ninterface Digit {\n  d: string;\n  s: [number, number];\n  e: [number, number];\n  rev?: boolean;\n}\n\nconst DIGITS: Record<string, Digit> = {\n  \"0\": {\n    d: \"M .31 0 C .5 0 .62 .2 .62 .5 C .62 .8 .5 1 .31 1 C .12 1 0 .8 0 .5 C 0 .2 .12 0 .31 0\",\n    s: [1, 0],\n    e: [1, 0],\n  },\n  \"1\": {\n    d: \"M .12 .22 L .38 0 L .38 1\",\n    s: [0.76, -0.65],\n    e: [0, 1],\n  },\n  \"2\": {\n    d: \"M .04 .24 C .08 .08 .2 0 .33 0 C .5 0 .6 .12 .6 .27 C .6 .42 .5 .52 .36 .64 L .02 1 L .62 1\",\n    s: [0.24, -0.97],\n    e: [1, 0],\n  },\n  \"3\": {\n    d: \"M .05 .12 C .14 .04 .23 0 .32 0 C .48 0 .58 .1 .58 .24 C .58 .38 .47 .46 .3 .46 C .5 .46 .62 .57 .62 .72 C .62 .89 .49 1 .31 1 C .18 1 .08 .94 .02 .85\",\n    s: [0.75, -0.66],\n    e: [0.35, 0.94],\n  },\n  \"4\": {\n    d: \"M .46 1 L .46 0 L 0 .7 L .64 .7\",\n    s: [0, -1],\n    e: [1, 0],\n  },\n  \"5\": {\n    d: \"M .58 0 L .1 0 L .05 .44 C .14 .38 .23 .36 .33 .36 C .5 .36 .62 .48 .62 .66 C .62 .86 .49 1 .3 1 C .17 1 .07 .94 .02 .86\",\n    s: [-1, 0],\n    e: [-0.53, -0.85],\n    rev: true,\n  },\n  \"6\": {\n    d: \"M .54 .06 C .47 .02 .4 0 .33 0 C .12 0 0 .22 0 .55 C 0 .84 .13 1 .31 1 C .5 1 .62 .86 .62 .67 C .62 .48 .49 .36 .32 .36 C .16 .36 .04 .46 0 .6\",\n    s: [-0.87, -0.5],\n    e: [-0.27, 0.96],\n    rev: true,\n  },\n  \"7\": {\n    d: \"M 0 0 L .62 0 C .45 .25 .3 .6 .25 1\",\n    s: [1, 0],\n    e: [0.45, 0.89],\n  },\n  \"8\": {\n    d: \"M .31 .46 C .14 .46 .04 .36 .04 .23 C .04 .1 .16 0 .31 0 C .46 0 .58 .1 .58 .23 C .58 .36 .48 .46 .31 .46 C .13 .46 0 .58 0 .73 C 0 .89 .13 1 .31 1 C .49 1 .62 .89 .62 .73 C .62 .58 .49 .46 .31 .46\",\n    s: [-1, 0],\n    e: [-1, 0],\n    rev: true,\n  },\n  \"9\": {\n    d: \"M .62 .38 C .58 .53 .46 .62 .3 .62 C .13 .62 0 .5 0 .31 C 0 .13 .13 0 .31 0 C .5 0 .62 .14 .62 .4 C .62 .76 .48 1 .24 1 C .16 1 .09 .98 .03 .95\",\n    s: [-0.26, 0.97],\n    e: [-0.9, -0.43],\n    rev: true,\n  },\n};\n\nconst ADV = 0.62;\nconst SPACE = 0.34;\nconst REACH = 0.26;\nconst JOIN_DY = 0.46;\nconst STROKE = 0.13;\n\nconst DRAW_AT = 16;\nconst DRAW_LEN = 46;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN_OUT = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_SWEEP = Easing.bezier(0.45, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction place(d: string, ox: number, oy: number, k: number): string {\n  const tokens = d.split(\" \").filter(Boolean);\n  const out: string[] = [];\n  let i = 0;\n  while (i < tokens.length) {\n    const tok = tokens[i];\n    if (/^[MLC]$/.test(tok)) {\n      out.push(tok);\n      i++;\n      continue;\n    }\n    const x = Number(tok);\n    const y = Number(tokens[i + 1]);\n    out.push(`${(ox + x * k).toFixed(2)} ${(oy + y * k).toFixed(2)}`);\n    i += 2;\n  }\n  return out.join(\" \");\n}\n\nfunction buildPath(chars: string[], k: number, ox: number, oy: number) {\n  const parts: string[] = [];\n  let x = ox;\n  let prevEnd: { p: [number, number]; dir: [number, number] } | null = null;\n  for (const ch of chars) {\n    const raw = DIGITS[ch];\n    if (!raw) continue;\n    const g: Digit = raw.rev\n      ? {\n          d: raw.d,\n          s: [-raw.e[0], -raw.e[1]],\n          e: [-raw.s[0], -raw.s[1]],\n        }\n      : raw;\n    const forward = place(g.d, x, oy, k);\n    const placed = raw.rev\n      ? reversePath(forward)\n          .replace(/([MLC])/g, \" $1 \")\n          .replace(/,/g, \" \")\n          .replace(/\\s+/g, \" \")\n          .trim()\n      : forward;\n    const first = placed.split(\" \");\n    const sx = Number(first[1]);\n    const sy = Number(first[2]);\n    if (prevEnd && Math.abs(sy - prevEnd.p[1]) <= JOIN_DY * k) {\n      const dist = Math.hypot(sx - prevEnd.p[0], sy - prevEnd.p[1]);\n      const r = Math.min(REACH * k * 1.5, dist * 0.42);\n      const c1x = prevEnd.p[0] + prevEnd.dir[0] * r;\n      const c1y = prevEnd.p[1] + prevEnd.dir[1] * r;\n      const c2x = sx - g.s[0] * r;\n      const c2y = sy - g.s[1] * r;\n      parts.push(\n        `C ${c1x.toFixed(2)} ${c1y.toFixed(2)} ${c2x.toFixed(2)} ${c2y.toFixed(2)} ${sx.toFixed(2)} ${sy.toFixed(2)}`,\n      );\n      parts.push(placed.replace(/^M \\S+ \\S+ ?/, \"\"));\n    } else {\n      parts.push(placed);\n    }\n    const nums = placed.split(\" \").filter((s) => !/^[MLC]$/.test(s));\n    const ex = Number(nums[nums.length - 2]);\n    const ey = Number(nums[nums.length - 1]);\n    prevEnd = { p: [ex, ey], dir: g.e };\n    x += (ADV + SPACE) * k;\n  }\n  return parts.join(\" \");\n}\n\nexport function SingleStroke({\n  value = \"27\",\n  unit = \"Mar\",\n  label = \"Demo day. Every team shows a film.\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: SingleStrokeProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const bg = background ?? c.bg;\n  const panelFill = theme === \"dark\" ? (accent ?? c.panel) : c.panel;\n  const strokeFill = theme === \"light\" ? (accent ?? c.stroke) : c.stroke;\n\n  const chars = useMemo(\n    () => Array.from(value).filter((ch) => DIGITS[ch] !== undefined),\n    [value],\n  );\n  const n = Math.max(1, chars.length);\n  const unitsW = n * ADV + (n - 1) * SPACE;\n  const portrait = H > W * 1.1;\n  const M = Math.min(W, H) * 0.075;\n  const split = portrait ? H * 0.62 : W * 0.6;\n  const fieldW = portrait ? W : split;\n  const fieldH = portrait ? split : H;\n  const k = Math.min(\n    (fieldW - M * 2.4) / (unitsW + STROKE),\n    (fieldH - M * 2.4) / (1 + STROKE),\n  );\n  const ox = fieldW / 2 - (unitsW * k) / 2;\n  const oy = fieldH / 2 - k / 2;\n\n  const d = useMemo(() => buildPath(chars, k, ox, oy), [chars, k, ox, oy]);\n  const len = useMemo(() => (d ? getLength(d) : 0), [d]);\n\n  const intro = interpolate(t, [0, 14], [0, 1], { ...CLAMP, easing: EASE_OUT });\n  const panelP = interpolate(t, [2, 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IN_OUT,\n  });\n  const p = interpolate(t, [DRAW_AT, DRAW_AT + DRAW_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_SWEEP,\n  });\n  const drawn = len * p;\n  const tip = len > 0 && p > 0 && p < 1 ? getPointAtLength(d, drawn) : null;\n  const tipFade = interpolate(p, [0, 0.04, 0.94, 1], [0, 1, 1, 0], CLAMP);\n\n  const sw = STROKE * k;\n  const hairW = Math.max(1.5, k * 0.012);\n  const unitP = interpolate(\n    t,\n    [DRAW_AT + DRAW_LEN - 10, DRAW_AT + DRAW_LEN + 8],\n    [0, 1],\n    { ...CLAMP, easing: EASE_OUT },\n  );\n  const labelAt = DRAW_AT + DRAW_LEN - 2;\n  const words = label ? label.split(\" \") : [];\n\n  const panelBox = portrait\n    ? { left: 0, top: split, width: W, height: H - split }\n    : { left: split, top: 0, width: W - split, height: H };\n  const unitSize = portrait ? W * 0.17 : H * 0.17;\n  const labelSize = portrait ? W * 0.056 : H * 0.056;\n  const text = {\n    fontFamily: archivo.fontFamily,\n    letterSpacing: \"-0.01em\",\n    color: c.panelInk,\n  };\n\n  return (\n    <AbsoluteFill style={{ background: bg, overflow: \"hidden\" }}>\n      <svg\n        width={W}\n        height={H}\n        viewBox={`0 0 ${W} ${H}`}\n        style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n        aria-hidden\n      >\n        <path\n          d={d}\n          fill=\"none\"\n          stroke={c.hair}\n          strokeWidth={hairW}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          opacity={intro * (1 - p * 0.6)}\n          pathLength={len}\n          strokeDasharray={`${len} ${len}`}\n          strokeDashoffset={len * (1 - intro)}\n        />\n        <path\n          d={d}\n          fill=\"none\"\n          stroke={strokeFill}\n          strokeWidth={sw}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeDasharray={`${drawn} ${len + sw}`}\n          opacity={p > 0 ? 1 : 0}\n        />\n        {tip ? (\n          <circle\n            cx={tip.x}\n            cy={tip.y}\n            r={sw * 0.22}\n            fill={c.tip}\n            opacity={0.8 * tipFade}\n          />\n        ) : null}\n      </svg>\n      <div\n        style={{\n          position: \"absolute\",\n          ...panelBox,\n          background: panelFill,\n          clipPath: portrait\n            ? `inset(${((1 - panelP) * 100).toFixed(3)}% 0 0 0)`\n            : `inset(0 0 0 ${((1 - panelP) * 100).toFixed(3)}%)`,\n        }}\n      >\n        {unit ? (\n          <div\n            style={{\n              ...text,\n              position: \"absolute\",\n              left: M,\n              top: M * 0.9,\n              fontVariationSettings: '\"wdth\" 125, \"wght\" 500',\n              fontSize: unitSize,\n              lineHeight: 1,\n              letterSpacing: \"-0.03em\",\n              whiteSpace: \"pre\",\n              opacity: unitP,\n              transform: `translateY(${((1 - unitP) * unitSize * 0.3).toFixed(2)}px)`,\n            }}\n          >\n            {unit}\n          </div>\n        ) : null}\n        {words.length > 0 ? (\n          <div\n            style={{\n              ...text,\n              position: \"absolute\",\n              left: M,\n              right: M,\n              bottom: M,\n              fontVariationSettings: '\"wdth\" 125, \"wght\" 420',\n              fontSize: labelSize,\n              lineHeight: 1.18,\n              textWrap: \"balance\",\n            }}\n          >\n            {words.map((w, i) => {\n              const q = interpolate(\n                t,\n                [labelAt + i * 3, labelAt + i * 3 + 16],\n                [0, 1],\n                { ...CLAMP, easing: EASE_OUT },\n              );\n              return (\n                <span key={`${w}-${i}`}>\n                  <span\n                    style={{\n                      display: \"inline-block\",\n                      opacity: q,\n                      transform: `translateY(${((1 - q) * labelSize * 0.6).toFixed(2)}px)`,\n                    }}\n                  >\n                    {w}\n                  </span>\n                  {i < words.length - 1 ? \" \" : \"\"}\n                </span>\n              );\n            })}\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/single-stroke.tsx"
    }
  ],
  "type": "registry:component"
}
