{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hand-count",
  "title": "Hand Count",
  "description": "A number that counts up in stop-motion poses, rewritten by hand at every pose and freezing still once it lands.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/stop-motion"],
  "files": [
    {
      "path": "registry/remocn/hand-count/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Caveat\";\nimport { useCurrentFrame } from \"remotion\";\nimport {\n  DEFAULT_STEP,\n  hashRange,\n  qstep,\n  steppedRamp,\n} from \"@/lib/remocn/stop-motion\";\n\nconst { fontFamily: HAND_FAMILY } = loadFont(\"normal\", {\n  subsets: [\"latin\"],\n  weights: [\"400\", \"500\", \"600\", \"700\"],\n});\n\nconst easeOutCubic = (t: number) => 1 - (1 - t) ** 3;\n\nconst easeInOutCubic = (t: number) =>\n  t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;\n\nconst DEFAULT_DURATION_STEPS = 18;\n\nexport type HandCountEase = \"in-out\" | \"out\" | \"linear\";\n\nconst EASES: Record<HandCountEase, (t: number) => number> = {\n  \"in-out\": easeInOutCubic,\n  out: easeOutCubic,\n  linear: (t: number) => t,\n};\n\nexport type HandCountTiming = {\n  delay?: number;\n  durationSteps?: number;\n  step?: number;\n};\n\nexport function handCountDuration(options?: HandCountTiming): number {\n  const delay = options?.delay ?? 0;\n  const durationSteps = options?.durationSteps ?? DEFAULT_DURATION_STEPS;\n  const step = options?.step ?? DEFAULT_STEP;\n  const end = delay + durationSteps * step;\n  return Math.ceil(end / step) * step;\n}\n\nexport function handCountValue(\n  args: HandCountTiming & {\n    frame: number;\n    to: number;\n    from?: number;\n    ease?: HandCountEase;\n  },\n): number {\n  const from = args.from ?? 0;\n  const delay = args.delay ?? 0;\n  const durationSteps = args.durationSteps ?? DEFAULT_DURATION_STEPS;\n  const step = args.step ?? DEFAULT_STEP;\n  const progress = steppedRamp(\n    args.frame,\n    delay,\n    delay + durationSteps * step,\n    {\n      ease: EASES[args.ease ?? \"in-out\"],\n      step,\n    },\n  );\n  return from + (args.to - from) * progress;\n}\n\nexport function handCountText(\n  value: number,\n  options?: { decimals?: number; prefix?: string; suffix?: string },\n): string {\n  return `${options?.prefix ?? \"\"}${value.toFixed(options?.decimals ?? 0)}${options?.suffix ?? \"\"}`;\n}\n\nexport function handCountPose(\n  frame: number,\n  options?: HandCountTiming,\n): number {\n  const step = options?.step ?? DEFAULT_STEP;\n  const settled = handCountDuration(options);\n  return Math.min(qstep(Math.max(0, frame), step), qstep(settled, step));\n}\n\nexport function handCountJitter(\n  args: HandCountTiming & { frame: number; index: number; seed?: string },\n): { rot: number; dy: number } {\n  const seed = args.seed ?? \"count\";\n  const pose = handCountPose(args.frame, args);\n  return {\n    rot: hashRange(`${seed}:${pose}:${args.index}:r`, -3.2, 3.2),\n    dy: hashRange(`${seed}:${pose}:${args.index}:y`, -1.8, 1.8),\n  };\n}\n\nexport interface HandCountProps {\n  to: number;\n  from?: number;\n  delay?: number;\n  durationSteps?: number;\n  decimals?: number;\n  prefix?: string;\n  suffix?: string;\n  fontSize?: number;\n  color?: string;\n  weight?: 400 | 500 | 600 | 700;\n  ease?: HandCountEase;\n  align?: \"left\" | \"center\";\n  fontFamily?: string;\n  seed?: string;\n  step?: number;\n}\n\nexport function HandCount({\n  to,\n  from = 0,\n  delay = 0,\n  durationSteps = DEFAULT_DURATION_STEPS,\n  decimals = 0,\n  prefix = \"\",\n  suffix = \"\",\n  fontSize = 96,\n  color = \"#26242c\",\n  weight = 700,\n  ease = \"in-out\",\n  align = \"center\",\n  fontFamily = HAND_FAMILY,\n  seed = \"count\",\n  step = DEFAULT_STEP,\n}: HandCountProps) {\n  const frame = useCurrentFrame();\n  const value = handCountValue({\n    frame,\n    to,\n    from,\n    delay,\n    durationSteps,\n    step,\n    ease,\n  });\n  const chars = Array.from(handCountText(value, { decimals, prefix, suffix }));\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: align === \"center\" ? \"center\" : \"flex-start\",\n        background: \"transparent\",\n      }}\n    >\n      <span\n        style={{\n          display: \"inline-block\",\n          fontFamily: `${fontFamily}, cursive`,\n          fontWeight: weight,\n          fontSize,\n          lineHeight: 1.15,\n          color,\n          whiteSpace: \"pre\",\n          textAlign: align,\n        }}\n      >\n        {chars.map((char, i) => {\n          const { rot, dy } = handCountJitter({\n            frame,\n            index: i,\n            seed,\n            delay,\n            durationSteps,\n            step,\n          });\n          return (\n            <span\n              key={`${i}:${char}`}\n              style={{\n                display: \"inline-block\",\n                whiteSpace: \"pre\",\n                translate: `0 ${dy}px`,\n                rotate: `${rot}deg`,\n              }}\n            >\n              {char}\n            </span>\n          );\n        })}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/hand-count.tsx"
    }
  ],
  "type": "registry:component"
}
