{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nonstop",
  "title": "Nonstop",
  "description": "A stops counter ticks down from three until the line resolves to the big phrase 'No stops'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/nonstop/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/RedHatDisplay\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst redHat = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface NonstopProps {\n  label?: string;\n  from?: number;\n  unit?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#B84A26\", ink: \"#FFF1E0\", accent: \"#8FD3FF\" },\n  light: { bg: \"#FFF1E0\", ink: \"#B84A26\", accent: \"#1B4F72\" },\n} as const;\n\nconst WGHT = 500;\nconst TRACK = -0.035;\nconst FONT_PX = 200;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst IN = 4;\nconst FIRST_STEP = 34;\nconst STEP = 20;\nconst ROLL = 10;\nconst ZERO_HOLD = 26;\nconst SHRINK = 14;\nconst BOARD_GAP = 6;\nconst CLOSE_GAP = 26;\nconst CLOSE = 13;\n\ntype Token = { id: string; ch: string };\n\ninterface StateLayout {\n  x: Record<string, number>;\n  width: number;\n}\n\ninterface Metrics {\n  states: StateLayout[];\n  adv: Record<string, number>;\n  asc: number;\n  desc: number;\n  fig: number;\n  xh: number;\n}\n\nfunction buildStates(from: number, unit: string): Token[][] {\n  const stem = unit.endsWith(\"s\") ? unit.slice(0, -1) : unit;\n  const stemTokens = Array.from(stem).map((ch, i) => ({ id: `u${i}`, ch }));\n  const states: Token[][] = [];\n  for (let v = from; v >= 0; v--) {\n    const row: Token[] = [\n      { id: \"d\", ch: String(v) },\n      { id: \"sp\", ch: \" \" },\n      ...stemTokens,\n    ];\n    if (v !== 1) row.push({ id: \"pl\", ch: \"s\" });\n    states.push(row);\n  }\n  states.push([\n    { id: \"N\", ch: \"N\" },\n    { id: \"d\", ch: \"o\" },\n    { id: \"sp\", ch: \" \" },\n    ...stemTokens,\n    { id: \"pl\", ch: \"s\" },\n  ]);\n  states.push([\n    { id: \"N\", ch: \"N\" },\n    { id: \"d\", ch: \"o\" },\n    { id: \"n\", ch: \"n\" },\n    ...stemTokens,\n    { id: \"dot\", ch: \".\" },\n  ]);\n  return states;\n}\n\nfunction measureStates(states: Token[][]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"nonstop: no 2d context\");\n  ctx.font = `${WGHT} ${FONT_PX}px \"${redHat.fontFamily}\"`;\n  const em = (s: string) =>\n    ctx.measureText(s).width / FONT_PX + TRACK * Array.from(s).length;\n  const adv: Record<string, number> = {};\n  const layouts = states.map((row) => {\n    const x: Record<string, number> = {};\n    let prefix = \"\";\n    for (const tk of row) {\n      x[tk.id] = em(prefix);\n      prefix += tk.ch;\n      adv[tk.ch] = em(tk.ch);\n    }\n    return { x, width: em(prefix) };\n  });\n  const fm = ctx.measureText(\"Hxg\");\n  const zero = ctx.measureText(\"0\");\n  const o = ctx.measureText(\"o\");\n  return {\n    states: layouts,\n    adv,\n    asc: fm.fontBoundingBoxAscent / FONT_PX,\n    desc: fm.fontBoundingBoxDescent / FONT_PX,\n    fig: zero.actualBoundingBoxAscent / FONT_PX,\n    xh: o.actualBoundingBoxAscent / FONT_PX,\n  };\n}\n\nfunction useMetrics(states: Token[][], label: string) {\n  const key = `${label}\\u0002${states.map((r) => r.map((tk) => `${tk.id}=${tk.ch}`).join(\"\\u0001\")).join(\"\\u0003\")}`;\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"nonstop: measuring\"));\n  const released = useRef(false);\n  const statesRef = useRef(states);\n  statesRef.current = states;\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      redHat.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${redHat.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measureStates(statesRef.current) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nexport function Nonstop({\n  label = \"From idea to export\",\n  from = 3,\n  unit = \"stops\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: NonstopProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const hi = accent ?? c.accent;\n  const stage =\n    background === \"transparent\" ? \"transparent\" : (background ?? c.bg);\n\n  const start = Math.max(1, Math.min(9, Math.round(from)));\n  const states = buildStates(start, unit);\n  const m = useMetrics(states, label);\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const countSteps = start;\n  const stepAt = (k: number) => FIRST_STEP + (k - 1) * STEP;\n  const zeroAt = stepAt(countSteps);\n  const shrinkAt = zeroAt + ROLL + ZERO_HOLD;\n  const boardAt = shrinkAt + BOARD_GAP;\n  const closeAt = boardAt + SHRINK + CLOSE_GAP;\n  const changeAt = [\n    ...Array.from({ length: countSteps }, (_, i) => stepAt(i + 1)),\n    shrinkAt,\n    closeAt,\n  ];\n  const durOf = (k: number) =>\n    k < countSteps ? ROLL : k === countSteps ? SHRINK + 4 : CLOSE;\n\n  let cur = 0;\n  let mix = 0;\n  for (let k = 0; k < changeAt.length; k++) {\n    if (t >= changeAt[k]) {\n      cur = k;\n      mix = interpolate(t, [changeAt[k], changeAt[k] + durOf(k)], [0, 1], {\n        ...CLAMP,\n        easing: EASE_IO,\n      });\n    }\n  }\n  const before = t < changeAt[0];\n  const prevIdx = before ? 0 : cur;\n  const nextIdx = before ? 0 : cur + 1;\n  const p = before ? 0 : mix;\n\n  const portrait = H > W * 1.1;\n  const marginX = W * (portrait ? 0.08 : 0.07);\n  const measureW = W - marginX * 2;\n  const widest = Math.max(...m.states.map((s) => s.width), 0.1);\n  const F = Math.min(H * (portrait ? 0.16 : 0.37), measureW / widest);\n  const right = W - marginX;\n  const base = H * (portrait ? 0.56 : 0.66);\n  const boxTop = base - ((1 - m.asc - m.desc) / 2 + m.asc) * F;\n  const L = Math.max(16, F * 0.17);\n\n  const lineIn = interpolate(t, [IN, IN + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const A = m.states[prevIdx];\n  const B = m.states[nextIdx];\n  const leftA = right - A.width * F;\n  const leftB = right - B.width * F;\n  const rowA = states[prevIdx];\n  const rowB = states[nextIdx];\n  const idsA = new Set(rowA.map((tk) => tk.id));\n  const idsB = new Set(rowB.map((tk) => tk.id));\n  const shrinking = prevIdx === countSteps;\n\n  const glyph = (\n    key: string,\n    ch: string,\n    x: number,\n    dy: number,\n    opacity: number,\n    color: string,\n    extra?: React.CSSProperties,\n  ) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: boxTop,\n        height: F,\n        lineHeight: `${F}px`,\n        whiteSpace: \"pre\",\n        fontFamily: redHat.fontFamily,\n        fontWeight: WGHT,\n        fontSize: F,\n        letterSpacing: `${TRACK}em`,\n        color,\n        opacity,\n        transform: `translateY(${dy.toFixed(2)}px)`,\n        ...extra,\n      }}\n    >\n      {ch}\n    </div>\n  );\n\n  const nodes: React.ReactNode[] = [];\n  const rise = F * 1.3;\n  const ids = new Set([...idsA, ...idsB]);\n  for (const id of ids) {\n    const ta = rowA.find((tk) => tk.id === id);\n    const tb = rowB.find((tk) => tk.id === id);\n    const color = id === \"d\" || id === \"dot\" ? hi : c.ink;\n    if (ta && tb) {\n      const xa = leftA + A.x[id] * F;\n      const xb = leftB + B.x[id] * F;\n      const x = xa + (xb - xa) * p;\n      if (ta.ch === tb.ch) {\n        if (id === \"sp\") continue;\n        nodes.push(glyph(id, ta.ch, x, 0, 1, color));\n      } else if (shrinking && id === \"d\") {\n        const zeroW = m.adv[ta.ch];\n        const oW = m.adv[tb.ch];\n        const sy = 1 + (m.xh / m.fig - 1) * p;\n        const sx = 1 + (oW / zeroW - 1) * p;\n        const swap = interpolate(p, [0.82, 1], [0, 1], CLAMP);\n        nodes.push(\n          glyph(`${id}-a`, ta.ch, x, 0, 1 - swap, color, {\n            transformOrigin: `${((zeroW * F) / 2).toFixed(2)}px ${(base - boxTop).toFixed(2)}px`,\n            transform: `scale(${sx.toFixed(4)}, ${sy.toFixed(4)})`,\n          }),\n        );\n        if (swap > 0) nodes.push(glyph(`${id}-b`, tb.ch, x, 0, swap, color));\n      } else {\n        const out = interpolate(p, [0, 0.45], [0, 1], {\n          ...CLAMP,\n          easing: Easing.bezier(0.55, 0, 1, 0.45),\n        });\n        const inn = interpolate(p, [0.4, 1], [0, 1], {\n          ...CLAMP,\n          easing: EASE_OUT,\n        });\n        const travel = (m.fig + 0.12) * F;\n        nodes.push(\n          <div\n            key={`${id}-clip`}\n            style={{\n              position: \"absolute\",\n              left: 0,\n              right: 0,\n              top: base - (m.fig + 0.05) * F,\n              height: (m.fig + 0.1) * F,\n              overflow: \"hidden\",\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                top: -(base - (m.fig + 0.05) * F),\n              }}\n            >\n              {out < 1\n                ? glyph(`${id}-a`, ta.ch, x, -travel * out, 1, color)\n                : null}\n              {inn > 0\n                ? glyph(`${id}-b`, tb.ch, x, travel * (1 - inn), 1, color)\n                : null}\n            </div>\n          </div>,\n        );\n      }\n    } else if (ta) {\n      if (ta.ch === \" \") continue;\n      const x = leftA + A.x[id] * F;\n      nodes.push(glyph(`${id}-a`, ta.ch, x, -rise * p, 1 - p, color));\n    } else if (tb) {\n      if (tb.ch === \" \") continue;\n      const x = leftB + B.x[id] * F;\n      let q = p;\n      if (id === \"N\") {\n        q = interpolate(t, [boardAt, boardAt + SHRINK], [0, 1], {\n          ...CLAMP,\n          easing: EASE_OUT,\n        });\n      }\n      nodes.push(glyph(`${id}-b`, tb.ch, x, rise * (1 - q), q, color));\n    }\n  }\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          right: marginX,\n          top: base - F * 0.98 - L * 1.5,\n          fontFamily: redHat.fontFamily,\n          fontWeight: 400,\n          fontSize: L,\n          lineHeight: 1,\n          letterSpacing: \"-0.01em\",\n          color: c.ink,\n          whiteSpace: \"pre\",\n          opacity: lineIn,\n          transform: `translateY(${((1 - lineIn) * L * 0.8).toFixed(2)}px)`,\n        }}\n      >\n        {label}\n      </div>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          clipPath: `inset(${(base - F * 0.98).toFixed(1)}px 0 ${(H - base - F * 0.3).toFixed(1)}px 0)`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            transform: `translateY(${((1 - lineIn) * F * 1.3).toFixed(2)}px)`,\n          }}\n        >\n          {nodes}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/nonstop.tsx"
    }
  ],
  "type": "registry:component"
}
