{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "duotone-beat",
  "title": "Duotone Beat",
  "description": "A two-ink halftone portrait swaps its ink pair on every beat, with a punch-in on the downbeat and a bar indicator.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/duotone-beat/index.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  Img,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\n/** [shadow ink, highlight ink], hex colors. */\nexport type DuotonePalette = [string, string];\n\nexport type DuotoneBeatProps = {\n  /** Portrait image. Without it a head-and-shoulders silhouette is drawn. */\n  src?: string;\n  /** Ink pairs, one per beat, cycled. Hex colors only (they feed an SVG LUT). */\n  palettes?: DuotonePalette[];\n  /** Frames per beat at 30fps. Ignored when `bpm` is set. */\n  beatFrames?: number;\n  bpm?: number;\n  /** Beats per bar; the first beat of each bar gets the bigger punch-in. */\n  beatsPerBar?: number;\n  /** Scale added on the downbeat. Off-beats get a third of it. */\n  punch?: number;\n  showIndicator?: boolean;\n  /** Halftone dot pitch in px at 720p for a printed look. 0 keeps smooth tones. */\n  halftone?: number;\n  speed?: number;\n};\n\nexport const duotoneBeatDefaults = {\n  palettes: [\n    [\"#111111\", \"#FFFFFF\"],\n    [\"#111111\", \"#E5322D\"],\n    [\"#E5322D\", \"#FFFFFF\"],\n    [\"#E5322D\", \"#111111\"],\n  ] as DuotonePalette[],\n  beatFrames: 15,\n  beatsPerBar: 4,\n  punch: 0.07,\n  showIndicator: true,\n  halftone: 8,\n  speed: 1,\n};\n\nconst MONO = \"ui-monospace, 'SF Mono', Menlo, Consolas, monospace\";\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  const h = hex.replace(\"#\", \"\").trim();\n  const full =\n    h.length === 3\n      ? h\n          .split(\"\")\n          .map((c) => c + c)\n          .join(\"\")\n      : h.slice(0, 6);\n  const n = Number.parseInt(full, 16);\n  if (Number.isNaN(n)) return [0, 0, 0];\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\nfunction Ground({\n  id,\n  width,\n  height,\n}: {\n  id: string;\n  width: number;\n  height: number;\n}) {\n  return (\n    <svg width={width} height={height} aria-hidden>\n      <defs>\n        <radialGradient id={`${id}-ground`} cx=\"0.5\" cy=\"0.4\" r=\"0.8\">\n          <stop offset=\"0\" stopColor=\"#FFFFFF\" />\n          <stop offset=\"0.5\" stopColor=\"#F2F2F2\" />\n          <stop offset=\"1\" stopColor=\"#C4C4C4\" />\n        </radialGradient>\n      </defs>\n      <rect width={width} height={height} fill={`url(#${id}-ground)`} />\n    </svg>\n  );\n}\n\nfunction Portrait({\n  id,\n  width,\n  height,\n}: {\n  id: string;\n  width: number;\n  height: number;\n}) {\n  const h = height;\n  const cx = width / 2;\n  const P = (x: number, y: number) => `${cx + x * h} ${y * h}`;\n  const head = [\n    `M ${P(0, 0.17)}`,\n    `C ${P(0.075, 0.17)} ${P(0.118, 0.23)} ${P(0.118, 0.32)}`,\n    `C ${P(0.118, 0.4)} ${P(0.095, 0.46)} ${P(0.06, 0.49)}`,\n    `C ${P(0.035, 0.51)} ${P(-0.035, 0.51)} ${P(-0.06, 0.49)}`,\n    `C ${P(-0.095, 0.46)} ${P(-0.118, 0.4)} ${P(-0.118, 0.32)}`,\n    `C ${P(-0.118, 0.23)} ${P(-0.075, 0.17)} ${P(0, 0.17)} Z`,\n  ].join(\" \");\n  const hair = [\n    `M ${P(-0.126, 0.34)}`,\n    `C ${P(-0.14, 0.17)} ${P(-0.05, 0.125)} ${P(0.02, 0.135)}`,\n    `C ${P(0.11, 0.14)} ${P(0.145, 0.21)} ${P(0.126, 0.32)}`,\n    `C ${P(0.11, 0.245)} ${P(0.06, 0.215)} ${P(-0.01, 0.222)}`,\n    `C ${P(-0.07, 0.228)} ${P(-0.105, 0.26)} ${P(-0.126, 0.34)} Z`,\n  ].join(\" \");\n  const torso = [\n    `M ${P(-0.07, 0.585)}`,\n    `C ${P(-0.13, 0.62)} ${P(-0.3, 0.63)} ${P(-0.36, 0.7)}`,\n    `C ${P(-0.41, 0.76)} ${P(-0.43, 0.88)} ${P(-0.44, 1.01)}`,\n    `L ${P(0.44, 1.01)}`,\n    `C ${P(0.43, 0.88)} ${P(0.41, 0.76)} ${P(0.36, 0.7)}`,\n    `C ${P(0.3, 0.63)} ${P(0.13, 0.62)} ${P(0.07, 0.585)} Z`,\n  ].join(\" \");\n  return (\n    <svg width={width} height={height} aria-hidden>\n      <defs>\n        <linearGradient id={`${id}-skin`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0.2\">\n          <stop offset=\"0\" stopColor=\"#121212\" />\n          <stop offset=\"0.55\" stopColor=\"#3A3A3A\" />\n          <stop offset=\"1\" stopColor=\"#9C9C9C\" />\n        </linearGradient>\n        <linearGradient id={`${id}-shirt`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n          <stop offset=\"0\" stopColor=\"#141414\" />\n          <stop offset=\"0.6\" stopColor=\"#262626\" />\n          <stop offset=\"1\" stopColor=\"#6A6A6A\" />\n        </linearGradient>\n      </defs>\n      <path\n        d={`M ${P(-0.055, 0.45)} L ${P(-0.065, 0.61)} L ${P(0.065, 0.61)} L ${P(0.055, 0.45)} Z`}\n        fill=\"#0C0C0C\"\n      />\n      <path d={torso} fill={`url(#${id}-shirt)`} />\n      <path\n        d={`M ${P(-0.075, 0.59)} Q ${P(0, 0.67)} ${P(0.075, 0.59)}`}\n        fill=\"none\"\n        stroke=\"#7A7A7A\"\n        strokeWidth={0.012 * h}\n      />\n      <ellipse\n        cx={cx - 0.12 * h}\n        cy={0.34 * h}\n        rx={0.018 * h}\n        ry={0.04 * h}\n        fill=\"#161616\"\n      />\n      <ellipse\n        cx={cx + 0.12 * h}\n        cy={0.34 * h}\n        rx={0.018 * h}\n        ry={0.04 * h}\n        fill=\"#8A8A8A\"\n      />\n      <path d={head} fill={`url(#${id}-skin)`} />\n      <path d={hair} fill=\"#060606\" />\n      {/* Rim light on the key side. */}\n      <g\n        fill=\"none\"\n        stroke=\"#D6D6D6\"\n        strokeWidth={0.007 * h}\n        strokeLinecap=\"round\"\n        style={{ filter: `blur(${0.003 * h}px)` }}\n      >\n        <path\n          d={`M ${P(0.112, 0.27)} C ${P(0.118, 0.36)} ${P(0.105, 0.43)} ${P(0.07, 0.475)}`}\n        />\n        <path\n          d={`M ${P(0.14, 0.625)} C ${P(0.3, 0.64)} ${P(0.39, 0.72)} ${P(0.42, 0.88)}`}\n        />\n      </g>\n    </svg>\n  );\n}\n\nexport function DuotoneBeat({\n  src,\n  palettes = duotoneBeatDefaults.palettes,\n  beatFrames = duotoneBeatDefaults.beatFrames,\n  bpm,\n  beatsPerBar = duotoneBeatDefaults.beatsPerBar,\n  punch = duotoneBeatDefaults.punch,\n  showIndicator = duotoneBeatDefaults.showIndicator,\n  halftone = duotoneBeatDefaults.halftone,\n  speed = duotoneBeatDefaults.speed,\n}: DuotoneBeatProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const u = Math.min(width, height) / 720;\n  const t = (frame / fps) * 30 * speed;\n  const period = Math.max(1, bpm ? (30 * 60) / bpm : beatFrames);\n  const beat = Math.floor(t / period);\n  const local = t - beat * period;\n  const bar = Math.max(1, Math.round(beatsPerBar));\n  const inBar = beat % bar;\n  const list = palettes.length > 0 ? palettes : duotoneBeatDefaults.palettes;\n  const [dark, light] = list[beat % list.length].map(hexToRgb);\n\n  const strength = inBar === 0 ? punch : punch / 3;\n  const scale = 1 + strength * Math.exp(-local / 3.2);\n  const rise = interpolate(t, [0, 12], [0.22, 0], {\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n  const intro = interpolate(t, [0, 12], [0, 1], {\n    extrapolateRight: \"clamp\",\n    easing: Easing.out(Easing.cubic),\n  });\n  const table = (i: number) => `${dark[i]} ${light[i]}`;\n  // Halftone screen: a radial distance tile, thresholded against the gray.\n  const pitch = halftone * u;\n  const tile =\n    pitch > 0\n      ? `data:image/svg+xml;utf8,${encodeURIComponent(\n          `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${pitch}\" height=\"${pitch}\"><defs><radialGradient id=\"g\" r=\"0.71\"><stop offset=\"0\" stop-color=\"#000\"/><stop offset=\"1\" stop-color=\"#fff\"/></radialGradient></defs><rect width=\"${pitch}\" height=\"${pitch}\" fill=\"url(#g)\"/></svg>`,\n        )}`\n      : null;\n  const lightHex = list[beat % list.length][1];\n  const darkHex = list[beat % list.length][0];\n\n  return (\n    <AbsoluteFill style={{ overflow: \"hidden\" }}>\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <filter\n          id={`${id}-duotone`}\n          x=\"0\"\n          y=\"0\"\n          width=\"1\"\n          height=\"1\"\n          colorInterpolationFilters=\"sRGB\"\n        >\n          <feColorMatrix type=\"saturate\" values=\"0\" result=\"gray\" />\n          {tile ? (\n            <>\n              <feImage\n                href={tile}\n                x={0}\n                y={0}\n                width={pitch}\n                height={pitch}\n                result=\"dot\"\n              />\n              <feTile in=\"dot\" result=\"screen\" />\n              {/* alpha: 8 - 8 + 1 = 1, color: 8 * (gray - screen) + 1 */}\n              <feComposite\n                in=\"gray\"\n                in2=\"screen\"\n                operator=\"arithmetic\"\n                k1={0}\n                k2={8}\n                k3={-8}\n                k4={1}\n              />\n            </>\n          ) : null}\n          <feComponentTransfer>\n            <feFuncR type=\"table\" tableValues={table(0)} />\n            <feFuncG type=\"table\" tableValues={table(1)} />\n            <feFuncB type=\"table\" tableValues={table(2)} />\n          </feComponentTransfer>\n        </filter>\n      </svg>\n      <AbsoluteFill style={{ filter: `url(#${id}-duotone)` }}>\n        <AbsoluteFill\n          style={{\n            backgroundColor: \"#FFFFFF\",\n            transform: `scale(${scale})`,\n            transformOrigin: \"50% 40%\",\n          }}\n        >\n          {src ? (\n            <Img\n              src={src}\n              style={{\n                width: \"100%\",\n                height: \"100%\",\n                objectFit: \"cover\",\n                transform: `scale(${1.12 - 0.12 * intro})`,\n              }}\n            />\n          ) : (\n            <>\n              <Ground id={id} width={width} height={height} />\n              <AbsoluteFill\n                style={{ transform: `translateY(${rise * height}px)` }}\n              >\n                <Portrait id={id} width={width} height={height} />\n              </AbsoluteFill>\n            </>\n          )}\n        </AbsoluteFill>\n      </AbsoluteFill>\n      {showIndicator ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 48 * u,\n            bottom: 44 * u,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 10 * u,\n          }}\n        >\n          {Array.from({ length: bar }, (_, i) => (\n            <div\n              key={i}\n              style={{\n                width: 26 * u,\n                height: 26 * u,\n                boxSizing: \"border-box\",\n                border: `${2.5 * u}px solid ${darkHex}`,\n                backgroundColor: i === inBar ? darkHex : \"transparent\",\n                transform: `scale(${i === inBar ? 1 + 0.25 * Math.exp(-local / 3) : 1})`,\n              }}\n            />\n          ))}\n          <span\n            style={{\n              marginLeft: 10 * u,\n              fontFamily: MONO,\n              fontSize: 20 * u,\n              fontWeight: 700,\n              letterSpacing: 2 * u,\n              color: darkHex,\n              backgroundColor: lightHex,\n            }}\n          >\n            {String(Math.round((30 * 60) / period))} BPM\n          </span>\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/duotone-beat.tsx"
    }
  ],
  "type": "registry:component"
}
