{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "midnight-guilloche",
  "title": "Guilloche rosette",
  "author": "Vilqor <https://vilqor.com>",
  "description": "An engraved guilloche rosette drawn as fine SVG lines, with sparks of light running along them, for a quiet background ornament.",
  "registryDependencies": [
    "utils",
    "https://midnight-lite.vilqor.com/r/midnight-theme.json"
  ],
  "files": [
    {
      "path": "src/components/site/guilloche.tsx",
      "content": "\"use client\"\n\nimport { useMounted } from \"@/hooks/use-mounted\"\nimport { cn } from \"@/lib/utils\"\nimport { ROPE, SPIRAL } from \"./guilloche-paths\"\n\n// The rosette is drawn by the browser once the page is up (its curves would otherwise make the HTML\n// several times heavier) and fades in. The <svg> itself is in the page from the start, so nothing moves.\n\n/** Engraved spiral rosette with its rope, in one quiet colour. Instead of turning, it is lit:\n *  sparks travel along the rope and along two spiral lines. `id` must be unique on the page. */\nexport function GuillocheSpiral({ id, className, lines = 10 }: { id: string; className?: string; lines?: number }) {\n  const mounted = useMounted()\n  const turn = (k: number) => `rotate(${((k * 360) / 7 / lines).toFixed(3)})`\n  return (\n    <svg viewBox=\"-180 -180 360 360\" fill=\"none\" aria-hidden=\"true\" className={cn(\"pointer-events-none select-none\", className)}>\n      {mounted && (\n        <g className=\"ornament-in\">\n          <defs>\n            <path id={`${id}-s`} d={SPIRAL} />\n            <path id={`${id}-r`} d={ROPE} />\n          </defs>\n          <g stroke=\"var(--ink)\" strokeOpacity=\"0.07\" strokeWidth=\"0.8\">\n            {Array.from({ length: lines }, (_, k) => (\n              <use key={k} href={`#${id}-s`} transform={turn(k)} vectorEffect=\"non-scaling-stroke\" />\n            ))}\n          </g>\n          <g stroke=\"var(--ink)\" strokeOpacity=\"0.13\" strokeWidth=\"0.9\">\n            <use href={`#${id}-r`} vectorEffect=\"non-scaling-stroke\" />\n            <use href={`#${id}-r`} transform={`rotate(${(360 / 28 / 2).toFixed(3)})`} vectorEffect=\"non-scaling-stroke\" />\n          </g>\n          {/* the sparks: short dashes that run the length of a line */}\n          <g stroke=\"var(--night-ink)\" strokeWidth=\"0.55\" strokeLinecap=\"round\">\n            <path d={ROPE} pathLength={100} className=\"glint-run\" />\n            <path d={SPIRAL} pathLength={100} transform={turn(2)} className=\"glint-run glint-run-slow\" style={{ animationDelay: \"-9s\" }} />\n            <path d={SPIRAL} pathLength={100} transform={turn(7)} className=\"glint-run glint-run-slow\" style={{ animationDelay: \"-22s\" }} />\n          </g>\n        </g>\n      )}\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/midnight/guilloche.tsx"
    },
    {
      "path": "src/components/site/guilloche-paths.ts",
      "content": "// The curves behind the guilloche rosette. Plain functions, no React.\n\n// The airy spiral: one curve r(θ) = 110 + 55·sin 7θ·cos 3θ, turned `lines` times through one petal,\n// circled by a two-strand rope r(θ) = 170 + 4·sin 28θ. Each curve is drawn once and reused.\nexport const polar = (r: (t: number) => number, steps = 540) => {\n  let d = \"\"\n  for (let i = 0; i < steps; i++) {\n    const t = (i / steps) * Math.PI * 2\n    const v = r(t)\n    d += `${i ? \"L\" : \"M\"}${(v * Math.cos(t)).toFixed(1)} ${(v * Math.sin(t)).toFixed(1)}`\n  }\n  return d + \"Z\"\n}\nexport const SPIRAL = polar((t) => 110 + 55 * Math.sin(7 * t) * Math.cos(3 * t))\nexport const ROPE = polar((t) => 170 + 4 * Math.sin(28 * t), 720)\n",
      "type": "registry:component",
      "target": "@components/midnight/guilloche-paths.ts"
    },
    {
      "path": "src/hooks/use-mounted.ts",
      "content": "import { useSyncExternalStore } from \"react\"\n\nconst subscribe = () => () => {}\n\n/** False on the server and while the page hydrates, true once it runs in the browser.\n *  For decoration the browser can draw itself, so it stays out of the HTML. */\nexport const useMounted = () =>\n  useSyncExternalStore(\n    subscribe,\n    () => true,\n    () => false,\n  )\n",
      "type": "registry:hook",
      "target": "@hooks/use-mounted.ts"
    }
  ],
  "categories": [
    "background"
  ],
  "type": "registry:component"
}