{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "registration-plate",
  "type": "registry:component",
  "title": "Registration Plate",
  "description": "A controlled React print study with three separable ink layers. Drag or use arrow keys to shift the plates; export the same state as standalone SVG. Uses authored RGB multiply blending, not spectral ink simulation.",
  "dependencies": [],
  "files": [
    {
      "path": "components/RegistrationPlate.tsx",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n\"use client\";\nimport { useRef, type KeyboardEvent, type PointerEvent } from \"react\";\nimport {\n  registrationSvg,\n  type RegistrationState,\n} from \"../lib/render/registration\";\n\nexport interface RegistrationPlateProps {\n  state: RegistrationState;\n  onChange?: (offset: Pick<RegistrationState, \"x\" | \"y\">) => void;\n}\n/** Controlled, dependency-free SVG print surface. Pointer and keyboard move identical state. */\nexport function RegistrationPlate({ state, onChange }: RegistrationPlateProps) {\n  const drag = useRef<{\n    x: number;\n    y: number;\n    startX: number;\n    startY: number;\n  } | null>(null);\n  function move(event: PointerEvent<HTMLDivElement>) {\n    if (!drag.current || !onChange) return;\n    const scale = 800 / event.currentTarget.getBoundingClientRect().width;\n    onChange({\n      x: Math.round(\n        Math.max(\n          -60,\n          Math.min(\n            60,\n            drag.current.startX + (event.clientX - drag.current.x) * scale,\n          ),\n        ),\n      ),\n      y: Math.round(\n        Math.max(\n          -60,\n          Math.min(\n            60,\n            drag.current.startY + (event.clientY - drag.current.y) * scale,\n          ),\n        ),\n      ),\n    });\n  }\n  function key(event: KeyboardEvent<HTMLDivElement>) {\n    if (\n      !onChange ||\n      ![\"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\", \"Home\"].includes(\n        event.key,\n      )\n    )\n      return;\n    event.preventDefault();\n    const step = event.shiftKey ? 10 : 1;\n    onChange(\n      event.key === \"Home\"\n        ? { x: 0, y: 0 }\n        : {\n            x: Math.max(\n              -60,\n              Math.min(\n                60,\n                state.x +\n                  (event.key === \"ArrowRight\"\n                    ? step\n                    : event.key === \"ArrowLeft\"\n                      ? -step\n                      : 0),\n              ),\n            ),\n            y: Math.max(\n              -60,\n              Math.min(\n                60,\n                state.y +\n                  (event.key === \"ArrowDown\"\n                    ? step\n                    : event.key === \"ArrowUp\"\n                      ? -step\n                      : 0),\n              ),\n            ),\n          },\n    );\n  }\n  return (\n    <div\n      role={onChange ? \"group\" : \"img\"}\n      tabIndex={onChange ? 0 : undefined}\n      aria-label={\n        onChange\n          ? \"Registration plate. Drag to shift the inks. Arrow keys move one unit, Shift moves ten. Home aligns offsets.\"\n          : \"Registration print specimen\"\n      }\n      onKeyDown={key}\n      onPointerDown={(event) => {\n        if (!onChange) return;\n        event.currentTarget.focus({ preventScroll: true });\n        event.currentTarget.setPointerCapture(event.pointerId);\n        drag.current = {\n          x: event.clientX,\n          y: event.clientY,\n          startX: state.x,\n          startY: state.y,\n        };\n      }}\n      onPointerMove={move}\n      onPointerUp={(event) => {\n        drag.current = null;\n        if (event.currentTarget.hasPointerCapture(event.pointerId))\n          event.currentTarget.releasePointerCapture(event.pointerId);\n      }}\n      onPointerCancel={() => {\n        drag.current = null;\n      }}\n      onLostPointerCapture={() => {\n        drag.current = null;\n      }}\n      style={{\n        width: \"100%\",\n        aspectRatio: \"1\",\n        lineHeight: 0,\n        cursor: onChange ? \"grab\" : undefined,\n        touchAction: onChange ? \"none\" : \"auto\",\n        overflow: \"hidden\",\n      }}\n      dangerouslySetInnerHTML={{\n        __html: registrationSvg(state).replace(\n          'width=\"800\" height=\"800\" role=',\n          'width=\"100%\" height=\"100%\" role=',\n        ),\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/RegistrationPlate.tsx"
    },
    {
      "path": "lib/render/registration.ts",
      "content": "/* Experience Design System influenced by Chromologium (https://chromologium.com/) */\n/** Authored screen-print study. RGB multiply previews overlap; it is not a spectral ink model.\n * Sourced from Chromologium (https://chromologium.com).\n */\nexport interface RegistrationState {\n  x: number;\n  y: number;\n  angle: number;\n  opacity: number;\n  motif: \"rosette\" | \"type\";\n  plates: [boolean, boolean, boolean];\n}\nexport const REGISTRATION_DEFAULT: RegistrationState = {\n  x: 12,\n  y: -8,\n  angle: 7,\n  opacity: 0.8,\n  motif: \"rosette\",\n  plates: [true, true, true],\n};\nexport const REGISTRATION_INKS = [\"#176dba\", \"#e04735\", \"#dfac32\"] as const;\nexport function parseRegistration(value: unknown): RegistrationState | null {\n  if (!value || typeof value !== \"object\") return null;\n  const v = value as Record<string, unknown>;\n  if (\n    ![v.x, v.y, v.angle, v.opacity].every(\n      (n) => typeof n === \"number\" && Number.isFinite(n),\n    )\n  )\n    return null;\n  if (v.motif !== \"rosette\" && v.motif !== \"type\") return null;\n  if (\n    !Array.isArray(v.plates) ||\n    v.plates.length !== 3 ||\n    !v.plates.every((p) => typeof p === \"boolean\")\n  )\n    return null;\n  const clamp = (n: unknown, lo: number, hi: number) =>\n    Math.min(hi, Math.max(lo, n as number));\n  return {\n    x: clamp(v.x, -60, 60),\n    y: clamp(v.y, -60, 60),\n    angle: clamp(v.angle, -30, 30),\n    opacity: clamp(v.opacity, 0.2, 1),\n    motif: v.motif,\n    plates: v.plates as RegistrationState[\"plates\"],\n  };\n}\nexport function registrationSvg(state: RegistrationState): string {\n  const s = parseRegistration(state) ?? REGISTRATION_DEFAULT;\n  const marks = [\n    [54, 54],\n    [746, 54],\n    [54, 746],\n    [746, 746],\n  ]\n    .map(\n      ([x, y]) =>\n        `<path d=\"M${x - 12} ${y}h24M${x} ${y - 12}v24\"/><circle cx=\"${x}\" cy=\"${y}\" r=\"7\"/>`,\n    )\n    .join(\"\");\n  const layers = REGISTRATION_INKS.map((ink, i) => {\n    if (!s.plates[i]) return \"\";\n    const factor = i - 1;\n    const artwork =\n      s.motif === \"type\"\n        ? '<text x=\"400\" y=\"555\" text-anchor=\"middle\" font-family=\"Georgia,serif\" font-size=\"510\" font-style=\"italic\" fill=\"currentColor\">a</text>'\n        : Array.from(\n            { length: 38 },\n            (_, n) =>\n              `<ellipse cx=\"400\" cy=\"400\" rx=\"${24 + n * 7.1}\" ry=\"${15 + n * 4.8}\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3.4\"/>`,\n          ).join(\"\");\n    return `<g data-plate=\"${i}\" style=\"mix-blend-mode:multiply\" color=\"${ink}\" opacity=\"${s.opacity}\" transform=\"translate(${factor * s.x} ${factor * s.y}) rotate(${-32 + factor * s.angle} 400 400)\">${artwork}</g>`;\n  }).join(\"\");\n  return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 800 800\" width=\"800\" height=\"800\" role=\"img\" aria-label=\"Three overlapping print separations\"><title>Registration study · Chromologium</title><desc>Authored RGB multiply study. x=${s.x}; y=${s.y}; angle=${s.angle}; opacity=${s.opacity}; motif=${s.motif}; plates=${s.plates.join(\",\")}. https://chromologium.com/lab/registration</desc><rect width=\"800\" height=\"800\" fill=\"#f2efdf\"/><g fill=\"none\" stroke=\"#a5a191\" stroke-width=\"0.9\">${marks}<path d=\"M400 32v18M400 750v18M32 400h18M750 400h18\"/></g><g style=\"isolation:isolate\">${layers}</g><g fill=\"#56554b\" font-family=\"monospace\" font-size=\"11\" letter-spacing=\"2\"><text x=\"76\" y=\"62\">CHROMOLOGIUM / PRINT STUDIES</text><text x=\"76\" y=\"739\">THREE INKS. ONE IMPRESSION.</text><text x=\"660\" y=\"739\">FIG. 015</text></g></svg>`;\n}\n",
      "type": "registry:lib",
      "target": "lib/render/registration.ts"
    }
  ],
  "docs": "Import RegistrationPlate from components/RegistrationPlate and REGISTRATION_DEFAULT, registrationSvg from lib/render/registration. Keep RegistrationState in React state; pass state and onChange={(offset) => setState(old => ({...old, ...offset}))}. Omit onChange for a static plate. registrationSvg(state) returns the complete export. No animation loop, external fonts, textures or WebGL required. The letterform uses the viewer’s installed serif font; the rosette is entirely vector geometry. Source: https://chromologium.com/lab/registration.",
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
