{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "landing-page-v3",
  "title": "Landing Page v3",
  "description": "Coming-soon countdown — a wide Solari split-flap board for Acme 2.0 where the headline, name, and live days/hours/minutes/seconds all live inside the grid as a single hero section. Flaps track the host light/dark tokens and blank cells recede into the page so the board sits without a bezel, with mechanical flips on power-up and every second as digits change. New projects only (shadcn init --template next).",
  "dependencies": [
    "geist"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/templates/landing-page-v3/app/layout.tsx",
      "content": "import type { Metadata } from \"next\"\nimport { GeistMono } from \"geist/font/mono\"\nimport { GeistSans } from \"geist/font/sans\"\nimport type { ReactNode } from \"react\"\n\nimport { site } from \"../lib/site\"\n\nimport \"./globals.css\"\n\nexport const metadata: Metadata = {\n  title: `${site.headline} ${site.name}`,\n  description: `${site.headline} ${site.name}`,\n}\n\nexport default function RootLayout({ children }: { children: ReactNode }) {\n  return (\n    <html\n      lang=\"en\"\n      className={`${GeistSans.variable} ${GeistMono.variable} h-full antialiased`}\n      suppressHydrationWarning\n    >\n      <body className=\"flex min-h-svh flex-col bg-background font-sans text-foreground\">\n        {children}\n      </body>\n    </html>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/layout.tsx"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/app/page.tsx",
      "content": "import { CountdownHero } from \"../components/countdown-hero\"\n\nexport default function HomePage() {\n  return (\n    <main>\n      <CountdownHero />\n    </main>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/page.tsx"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/styles/motion.css",
      "content": "/* landing-page-v3 motion.\n *\n * Theme tokens (:root / .dark / @theme) come from the shadcn starter's\n * app/globals.css — this file only carries the hero entrance animation,\n * scoped to its own class so it is safe to import anywhere (the component\n * that uses it, and the host preview route).\n *\n * Imported by components/countdown-hero.tsx.\n */\n\n@keyframes hero-reveal {\n  from {\n    opacity: 0;\n    filter: blur(10px);\n    transform: translateY(14px);\n  }\n  to {\n    opacity: 1;\n    filter: blur(0);\n    transform: translateY(0);\n  }\n}\n\n.hero-reveal {\n  opacity: 0;\n  animation: hero-reveal 900ms cubic-bezier(0.16, 1, 0.3, 1) both;\n  animation-delay: var(--hero-delay, 0ms);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .hero-reveal {\n    opacity: 1;\n    animation: none;\n    filter: none;\n    transform: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/motion.css"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/components/countdown-hero.tsx",
      "content": "\"use client\"\n\nimport {\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n} from \"react\"\n\nimport {\n  activeUnitKeys,\n  BOARD_COLS,\n  layoutBoard,\n  UNIT_KEYS,\n  unitValue,\n  type UnitKey,\n} from \"../lib/board\"\nimport { remainingParts, type CountdownParts } from \"../lib/countdown\"\nimport { site } from \"../lib/site\"\nimport { SolariBoard } from \"./solari-board\"\nimport { SOLARI_FLIP_MS } from \"./solari-cell\"\n\nimport \"../styles/motion.css\"\n\nconst SPIN_GLYPHS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\nconst ZERO_PARTS: CountdownParts = { days: 0, hours: 0, minutes: 0, seconds: 0 }\n\nconst SHUFFLE_STEPS = 4\nconst STAGGER_MS = 4\nconst UNIT_FLAP_STAGGER_MS = 42\n\nconst emptySubscribe = () => () => {}\n\nfunction useHydrated() {\n  return useSyncExternalStore(emptySubscribe, () => true, () => false)\n}\n\nfunction useReducedMotion() {\n  return useSyncExternalStore(\n    emptySubscribe,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false\n  )\n}\n\nfunction randomGlyph(exclude: string): string {\n  let glyph = SPIN_GLYPHS[Math.floor(Math.random() * SPIN_GLYPHS.length)] ?? \"A\"\n  let guard = 0\n  while (glyph === exclude && guard++ < 5) {\n    glyph = SPIN_GLYPHS[Math.floor(Math.random() * SPIN_GLYPHS.length)] ?? \"A\"\n  }\n  return glyph\n}\n\ntype UnitOverride = { key: UnitKey; left: string; right: string }\n\ntype QueuedOp = { type: \"drop\" | \"add\"; key: UnitKey }\n\nfunction useUnitRows(\n  parts: CountdownParts,\n  enabled: boolean,\n  reducedMotion: boolean\n) {\n  const [activeKeys, setActiveKeys] = useState<readonly UnitKey[]>(UNIT_KEYS)\n  const [override, setOverride] = useState<UnitOverride | null>(null)\n  const [queueTick, setQueueTick] = useState(0)\n  const [initialized, setInitialized] = useState(false)\n  const activeKeysRef = useRef(activeKeys)\n  const partsRef = useRef(parts)\n  const lastVisibleValuesRef = useRef<CountdownParts>(parts)\n  const busyRef = useRef(false)\n  const queueRef = useRef<QueuedOp[]>([])\n  const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])\n  const drainQueueRef = useRef<() => void>(() => {})\n  const snapToRef = useRef<(desired: readonly UnitKey[]) => void>(() => {})\n\n  if (enabled && !initialized) {\n    setInitialized(true)\n    setActiveKeys(activeUnitKeys(parts))\n  }\n\n  useEffect(() => {\n    partsRef.current = parts\n    for (const key of UNIT_KEYS) {\n      if (key === \"seconds\" || parts[key] !== 0) {\n        lastVisibleValuesRef.current[key] = parts[key]\n      }\n    }\n  })\n\n  useEffect(() => {\n    activeKeysRef.current = activeKeys\n  }, [activeKeys])\n\n  const clearTimers = () => {\n    for (const t of timersRef.current) clearTimeout(t)\n    timersRef.current = []\n  }\n\n  const unitCopy = (key: UnitKey) => ({\n    left: unitValue(key, {\n      ...partsRef.current,\n      [key]: lastVisibleValuesRef.current[key],\n    }),\n    right: (site.units[UNIT_KEYS.indexOf(key)] ?? \"\").toUpperCase(),\n  })\n\n  const replaceChar = (value: string, index: number, char: string) =>\n    `${value.slice(0, index)}${char}${value.slice(index + 1)}`\n\n  const setOverrideChar = (\n    key: UnitKey,\n    side: \"left\" | \"right\",\n    index: number,\n    char: string\n  ) => {\n    setOverride((current) => {\n      if (!current || current.key !== key) return current\n      return {\n        ...current,\n        [side]: replaceChar(current[side], index, char),\n      }\n    })\n  }\n\n  const scheduleFlapScramble = (\n    key: UnitKey,\n    copy: { left: string; right: string },\n    direction: \"out\" | \"in\",\n    startDelay = 0\n  ) => {\n    const flaps = ([\"left\", \"right\"] as const).flatMap((side) =>\n      copy[side].split(\"\").map((char, index) => ({ side, index, char }))\n    )\n\n    flaps.forEach(({ side, index, char }, flapIndex) => {\n      const base = startDelay + flapIndex * UNIT_FLAP_STAGGER_MS\n      let previous = direction === \"out\" ? char : \" \"\n\n      for (let step = 0; step < SHUFFLE_STEPS; step++) {\n        const glyph = randomGlyph(previous)\n        previous = glyph\n        timersRef.current.push(\n          setTimeout(\n            () => setOverrideChar(key, side, index, glyph),\n            base + step * SOLARI_FLIP_MS.fast\n          )\n        )\n      }\n\n      timersRef.current.push(\n        setTimeout(\n          () =>\n            setOverrideChar(\n              key,\n              side,\n              index,\n              direction === \"out\" ? \" \" : char\n            ),\n          base + SHUFFLE_STEPS * SOLARI_FLIP_MS.fast\n        )\n      )\n    })\n\n    const lastFlapDelay = Math.max(0, flaps.length - 1) * UNIT_FLAP_STAGGER_MS\n    return (\n      startDelay +\n      lastFlapDelay +\n      (SHUFFLE_STEPS + 1) * SOLARI_FLIP_MS.fast\n    )\n  }\n\n  const release = (delay: number) => {\n    timersRef.current.push(\n      setTimeout(() => {\n        busyRef.current = false\n        setQueueTick((n) => n + 1)\n      }, delay)\n    )\n  }\n\n  const runDrop = (key: UnitKey) => {\n    busyRef.current = true\n    clearTimers()\n\n    const copy = unitCopy(key)\n    setOverride({ key, ...copy })\n    const scrambleMs = scheduleFlapScramble(key, copy, \"out\")\n\n    timersRef.current.push(\n      setTimeout(() => {\n        setOverride(null)\n        setActiveKeys(activeKeysRef.current.filter((k) => k !== key))\n      }, scrambleMs)\n    )\n\n    release(scrambleMs)\n  }\n\n  const runAdd = (key: UnitKey) => {\n    busyRef.current = true\n    clearTimers()\n\n    const copy = unitCopy(key)\n    const withKey = UNIT_KEYS.filter(\n      (k) => k === key || activeKeysRef.current.includes(k)\n    )\n\n    setOverride({\n      key,\n      left: \" \".repeat(copy.left.length),\n      right: \" \".repeat(copy.right.length),\n    })\n    setActiveKeys(withKey)\n\n    const scrambleMs = scheduleFlapScramble(key, copy, \"in\")\n\n    timersRef.current.push(\n      setTimeout(() => setOverride(null), scrambleMs)\n    )\n\n    release(scrambleMs)\n  }\n\n  const drainQueue = () => {\n    if (busyRef.current) return\n\n    while (queueRef.current.length > 0) {\n      const op = queueRef.current.shift()!\n      const current = activeKeysRef.current\n      const desired = activeUnitKeys(partsRef.current)\n\n      if (op.type === \"drop\") {\n        if (!current.includes(op.key) || desired.includes(op.key)) continue\n        runDrop(op.key)\n        return\n      }\n\n      if (current.includes(op.key) || !desired.includes(op.key)) continue\n      runAdd(op.key)\n      return\n    }\n  }\n\n  const enqueue = (op: QueuedOp) => {\n    queueRef.current = queueRef.current.filter(({ key }) => key !== op.key)\n    queueRef.current.push(op)\n    queueRef.current.sort(\n      (a, b) => UNIT_KEYS.indexOf(a.key) - UNIT_KEYS.indexOf(b.key)\n    )\n  }\n\n  const snapTo = (desired: readonly UnitKey[]) => {\n    clearTimers()\n    queueRef.current = []\n    busyRef.current = false\n    queueMicrotask(() => {\n      setOverride(null)\n      setActiveKeys(desired)\n    })\n  }\n\n  useEffect(() => {\n    drainQueueRef.current = drainQueue\n    snapToRef.current = snapTo\n  })\n\n  useEffect(() => {\n    if (!enabled || !initialized) return\n\n    const desired = activeUnitKeys(parts)\n\n    if (reducedMotion) {\n      snapToRef.current(desired)\n      return\n    }\n\n    const current = activeKeysRef.current\n\n    for (const key of current) {\n      if (!desired.includes(key)) enqueue({ type: \"drop\", key })\n    }\n    for (const key of desired) {\n      if (!current.includes(key)) enqueue({ type: \"add\", key })\n    }\n\n    drainQueueRef.current()\n  }, [parts, enabled, reducedMotion, initialized])\n\n  useEffect(() => {\n    if (!enabled || busyRef.current) return\n    if (queueRef.current.length === 0) return\n    drainQueueRef.current()\n  }, [enabled, queueTick])\n\n  useEffect(() => {\n    return () => {\n      clearTimers()\n    }\n  }, [])\n\n  return { activeKeys, override }\n}\n\nfunction useBoardReveal(\n  target: string[] | null,\n  cols: number,\n  reducedMotion: boolean\n) {\n  const [rows, setRows] = useState<string[] | null>(null)\n  const [done, setDone] = useState(false)\n\n  useEffect(() => {\n    if (!target) return\n\n    const blank = \" \".repeat(cols)\n    setRows(target.map(() => blank))\n\n    const timers: ReturnType<typeof setTimeout>[] = []\n\n    if (reducedMotion) {\n      timers.push(\n        setTimeout(() => {\n          setRows(target)\n          setDone(true)\n        }, 0)\n      )\n      return () => {\n        for (const t of timers) clearTimeout(t)\n      }\n    }\n\n    const setCell = (r: number, c: number, char: string) => {\n      setRows((prev) => {\n        if (!prev) return prev\n        const next = [...prev]\n        const chars = next[r]!.split(\"\")\n        chars[c] = char\n        next[r] = chars.join(\"\")\n        return next\n      })\n    }\n\n    let cellIndex = 0\n    let lastLandDelay = 0\n\n    target.forEach((rowStr, r) => {\n      rowStr.split(\"\").forEach((finalChar, c) => {\n        const index = cellIndex++\n        if (finalChar === \" \") return\n\n        const base = index * STAGGER_MS\n        let prevGlyph = \" \"\n\n        for (let step = 0; step < SHUFFLE_STEPS; step++) {\n          const glyph = randomGlyph(prevGlyph)\n          prevGlyph = glyph\n          timers.push(\n            setTimeout(\n              () => setCell(r, c, glyph),\n              base + step * SOLARI_FLIP_MS.fast\n            )\n          )\n        }\n\n        const landDelay = base + SHUFFLE_STEPS * SOLARI_FLIP_MS.fast\n        lastLandDelay = Math.max(lastLandDelay, landDelay)\n        timers.push(setTimeout(() => setCell(r, c, finalChar), landDelay))\n      })\n    })\n\n    timers.push(\n      setTimeout(\n        () => setDone(true),\n        lastLandDelay + SOLARI_FLIP_MS.fast + 100\n      )\n    )\n\n    return () => {\n      for (const t of timers) clearTimeout(t)\n    }\n  }, [target, cols, reducedMotion])\n\n  return { rows, done }\n}\n\nexport function CountdownHero() {\n  const hydrated = useHydrated()\n  const [parts, setParts] = useState<CountdownParts>(ZERO_PARTS)\n  const [revealTarget, setRevealTarget] = useState<string[] | null>(null)\n  const [revealRowIds, setRevealRowIds] = useState<string[] | null>(null)\n\n  useEffect(() => {\n    if (!hydrated) return\n\n    const startId = window.setTimeout(() => {\n      const initialParts = remainingParts(site.targetDate, new Date())\n      setParts(initialParts)\n      const board = layoutBoard(site, initialParts)\n      setRevealTarget(board.rows)\n      setRevealRowIds(board.rowIds)\n    }, 0)\n\n    const tickId = window.setInterval(() => {\n      setParts(remainingParts(site.targetDate, new Date()))\n    }, 1000)\n\n    return () => {\n      clearTimeout(startId)\n      clearInterval(tickId)\n    }\n  }, [hydrated])\n\n  const reducedMotion = useReducedMotion()\n\n  const { activeKeys, override } = useUnitRows(\n    parts,\n    revealTarget !== null,\n    reducedMotion\n  )\n\n  const live = useMemo(() => {\n    const unitOverrides = override\n      ? { [override.key]: { left: override.left, right: override.right } }\n      : undefined\n    return layoutBoard(site, parts, BOARD_COLS, {\n      unitKeys: activeKeys,\n      unitOverrides,\n    })\n  }, [parts, activeKeys, override])\n\n  const { rows: revealRows, done: revealDone } = useBoardReveal(\n    revealTarget,\n    live.cols,\n    reducedMotion\n  )\n\n  const blankRows = useMemo(\n    () => live.rows.map(() => \" \".repeat(live.cols)),\n    [live]\n  )\n\n  const boardRows = revealDone ? live.rows : (revealRows ?? blankRows)\n  const boardRowIds = revealDone\n    ? live.rowIds\n    : (revealRowIds ?? live.rowIds)\n\n  const ariaLabel = hydrated\n    ? `${site.headline} ${site.name}. ${parts.days} days, ${parts.hours} hours, ${parts.minutes} minutes, ${parts.seconds} seconds until ${site.name}.`\n    : `${site.headline} ${site.name}.`\n\n  return (\n    <section className=\"relative h-dvh w-full overflow-hidden bg-background text-foreground\">\n      <div\n        className=\"hero-reveal h-full w-full\"\n        style={{ \"--hero-delay\": \"60ms\" } as CSSProperties}\n      >\n        <SolariBoard\n          rows={boardRows}\n          rowIds={boardRowIds}\n          cols={live.cols}\n          instant={reducedMotion}\n          speed={revealDone ? \"normal\" : \"fast\"}\n          fill\n          ariaLabel={ariaLabel}\n        />\n      </div>\n\n      <p className=\"sr-only\" aria-live=\"polite\">\n        {hydrated\n          ? `${parts.days} days, ${parts.hours} hours, ${parts.minutes} minutes, ${parts.seconds} seconds remaining until ${site.name}`\n          : `Countdown until ${site.name}`}\n      </p>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/countdown-hero.tsx"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/components/solari-board.tsx",
      "content": "\"use client\"\n\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n} from \"react\"\n\nimport {\n  boardGapPx,\n  boardNaturalSize,\n  fitBoardFrame,\n  fitBoardScale,\n  padBoard,\n  TARGET_CELL_PX,\n} from \"../lib/board\"\nimport { SolariCell } from \"./solari-cell\"\n\nimport \"../styles/solari.css\"\n\nconst TILE_SETTLE_MS = 140\n\ntype Frame = {\n  cols: number\n  rows: number\n  gap: number\n}\n\ntype SolariBoardProps = {\n  rows: string[]\n  rowIds?: string[]\n  cols: number\n  instant?: boolean\n  speed?: \"normal\" | \"fast\"\n  fill?: boolean\n  ariaLabel: string\n}\n\nfunction readRem(): number {\n  return (\n    Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n  )\n}\n\nexport function SolariBoard({\n  rows,\n  rowIds,\n  cols,\n  instant = false,\n  speed = \"normal\",\n  fill = false,\n  ariaLabel,\n}: SolariBoardProps) {\n  const rootRef = useRef<HTMLDivElement>(null)\n  const sizerRef = useRef<HTMLDivElement>(null)\n  const gridRef = useRef<HTMLDivElement>(null)\n  const initialFrame: Frame = { cols, rows: rows.length, gap: 4 }\n  const frameRef = useRef<Frame>(initialFrame)\n  const [frame, setFrame] = useState<Frame>(initialFrame)\n  const [measured, setMeasured] = useState(!fill)\n  const [snap, setSnap] = useState(false)\n\n  const ids =\n    rowIds && rowIds.length === rows.length\n      ? rowIds\n      : rows.map((_, i) => `row-${i}`)\n\n  useLayoutEffect(() => {\n    if (!fill) return\n    const el = rootRef.current\n    if (!el) return\n\n    const paintScale = (\n      tileCols: number,\n      tileRows: number,\n      gap: number,\n      width: number,\n      height: number\n    ) => {\n      const natural = boardNaturalSize(tileCols, tileRows, gap)\n      const scale = fitBoardScale(width, height, tileCols, tileRows, gap)\n      const sizer = sizerRef.current\n      const grid = gridRef.current\n      if (sizer) {\n        sizer.style.width = `${natural.width * scale}px`\n        sizer.style.height = `${natural.height * scale}px`\n      }\n      if (grid) {\n        grid.style.width = `${natural.width}px`\n        grid.style.gap = `${gap}px`\n        grid.style.transform = `scale(${scale})`\n      }\n    }\n\n    const applyTiles = () => {\n      const gap = boardGapPx(el.clientWidth, readRem())\n      const tiles = fitBoardFrame(\n        el.clientWidth,\n        el.clientHeight,\n        cols,\n        rows.length,\n        gap\n      )\n      const next = { cols: tiles.cols, rows: tiles.rows, gap }\n      const changed =\n        next.cols !== frameRef.current.cols ||\n        next.rows !== frameRef.current.rows ||\n        next.gap !== frameRef.current.gap\n      paintScale(next.cols, next.rows, next.gap, el.clientWidth, el.clientHeight)\n      setMeasured(true)\n      if (!changed) return\n      frameRef.current = next\n      setSnap(true)\n      setFrame(next)\n    }\n\n    applyTiles()\n\n    let tileTimer: ReturnType<typeof setTimeout> | undefined\n    const observer = new ResizeObserver(() => {\n      const gap = boardGapPx(el.clientWidth, readRem())\n      paintScale(\n        frameRef.current.cols,\n        frameRef.current.rows,\n        gap,\n        el.clientWidth,\n        el.clientHeight\n      )\n      if (tileTimer) clearTimeout(tileTimer)\n      tileTimer = setTimeout(applyTiles, TILE_SETTLE_MS)\n    })\n    observer.observe(el)\n    return () => {\n      observer.disconnect()\n      if (tileTimer) clearTimeout(tileTimer)\n    }\n  }, [fill, cols, rows.length])\n\n  useEffect(() => {\n    if (!snap) return\n    const id = requestAnimationFrame(() => setSnap(false))\n    return () => cancelAnimationFrame(id)\n  }, [snap])\n\n  const layout = fill\n    ? padBoard({ rows, cols, rowIds: ids }, frame.cols, frame.rows)\n    : { rows, cols, rowIds: ids }\n\n  const style: CSSProperties | undefined = fill\n    ? {\n        opacity: measured ? 1 : 0,\n      }\n    : undefined\n\n  const grid = (\n    <div\n      ref={fill ? gridRef : undefined}\n      className=\"solari-grid\"\n      style={\n        {\n          gridTemplateColumns: fill\n            ? `repeat(${layout.cols}, ${TARGET_CELL_PX}px)`\n            : `repeat(${layout.cols}, minmax(0, 1fr))`,\n          gap: fill ? frame.gap : undefined,\n          ...(fill ? { \"--solari-cell-size\": `${TARGET_CELL_PX}px` } : {}),\n        } as CSSProperties\n      }\n    >\n      {layout.rows.map((row, r) => {\n        const id = layout.rowIds[r] ?? `row-${r}`\n        return (\n          <div\n            key={id}\n            className=\"solari-row\"\n            style={{\n              gridColumn: \"1 / -1\",\n              display: \"grid\",\n              gridTemplateColumns: \"subgrid\",\n            }}\n          >\n            {row\n              .padEnd(layout.cols, \" \")\n              .slice(0, layout.cols)\n              .split(\"\")\n              .map((ch, c) => (\n                <SolariCell\n                  key={`${id}-${c}`}\n                  value={ch}\n                  instant={instant || snap}\n                  speed={speed}\n                />\n              ))}\n          </div>\n        )\n      })}\n    </div>\n  )\n\n  return (\n    <div\n      ref={rootRef}\n      className={fill ? \"solari-board solari-board-fill\" : \"solari-board\"}\n      style={style}\n      role=\"img\"\n      aria-label={ariaLabel}\n    >\n      <svg className=\"solari-defs\" aria-hidden>\n        <filter\n          id=\"solari-paper-grain\"\n          x=\"0%\"\n          y=\"0%\"\n          width=\"100%\"\n          height=\"100%\"\n        >\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.85\"\n            numOctaves=\"4\"\n            stitchTiles=\"stitch\"\n          />\n        </filter>\n      </svg>\n      {fill ? (\n        <div ref={sizerRef} className=\"solari-sizer\">\n          {grid}\n        </div>\n      ) : (\n        grid\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/solari-board.tsx"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/components/solari-cell.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\"\n\nexport const SOLARI_FLIP_MS = { normal: 260, fast: 130 } as const\n\ntype SolariCellProps = {\n  value: string\n  instant?: boolean\n  speed?: \"normal\" | \"fast\"\n}\n\nexport function SolariEmpty() {\n  const nbsp = String.fromCharCode(160)\n  return (\n    <div className=\"solari-cell solari-cell-empty\" aria-hidden>\n      <div className=\"solari-half solari-half-top solari-static-top\">\n        <span className=\"solari-glyph\">{nbsp}</span>\n      </div>\n      <div className=\"solari-half solari-half-bottom solari-static-bottom\">\n        <span className=\"solari-glyph\">{nbsp}</span>\n      </div>\n    </div>\n  )\n}\n\nexport function SolariCell({\n  value,\n  instant = false,\n  speed = \"normal\",\n}: SolariCellProps) {\n  const empty = value === \" \" || value === \"\"\n  const display = empty ? \" \" : value\n  const [current, setCurrent] = useState(display)\n  const [previous, setPrevious] = useState(display)\n  const [flipping, setFlipping] = useState(false)\n  const [flipSeq, setFlipSeq] = useState(0)\n  const currentRef = useRef(display)\n  const flipTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const flipMs = SOLARI_FLIP_MS[speed]\n\n  useEffect(() => {\n    if (display === currentRef.current) return\n\n    const startId = window.setTimeout(() => {\n      if (instant) {\n        currentRef.current = display\n        setPrevious(display)\n        setCurrent(display)\n        setFlipping(false)\n        return\n      }\n\n      const from = currentRef.current\n      currentRef.current = display\n      setPrevious(from)\n      setCurrent(display)\n      setFlipping(true)\n      setFlipSeq((n) => n + 1)\n\n      if (flipTimer.current) clearTimeout(flipTimer.current)\n      flipTimer.current = setTimeout(() => {\n        setFlipping(false)\n        setPrevious(display)\n      }, flipMs)\n    }, 0)\n\n    return () => {\n      clearTimeout(startId)\n      if (flipTimer.current) clearTimeout(flipTimer.current)\n    }\n  }, [display, instant, flipMs])\n\n  const showFlip = flipping && previous !== current\n  const style =\n    speed === \"fast\"\n      ? ({ \"--solari-duration\": `${flipMs}ms` } as CSSProperties)\n      : undefined\n\n  return (\n    <div\n      className={`solari-cell${empty ? \" solari-cell-empty\" : \"\"}`}\n      style={style}\n      aria-hidden\n    >\n      <div className=\"solari-half solari-half-top solari-static-top\">\n        <span className=\"solari-glyph\">{current}</span>\n      </div>\n      <div className=\"solari-half solari-half-bottom solari-static-bottom\">\n        <span className=\"solari-glyph\">{showFlip ? previous : current}</span>\n      </div>\n\n      {showFlip ? (\n        <>\n          <div\n            key={`top-${flipSeq}`}\n            className=\"solari-half solari-half-top solari-flip-top\"\n          >\n            <span className=\"solari-glyph\">{previous}</span>\n          </div>\n          <div\n            key={`bottom-${flipSeq}`}\n            className=\"solari-half solari-half-bottom solari-flip-bottom\"\n          >\n            <span className=\"solari-glyph\">{current}</span>\n          </div>\n        </>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/solari-cell.tsx"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/lib/board.ts",
      "content": "import { padDigits, type CountdownParts } from \"./countdown\"\n\nexport const BOARD_COLS = 23\n\nconst MAX_HEADING_LINES = 3\nconst MAX_NAME_LINES = 2\n\nexport const UNIT_KEYS = [\"days\", \"hours\", \"minutes\", \"seconds\"] as const\nexport type UnitKey = (typeof UNIT_KEYS)[number]\n\nconst UNIT_PADDED: Record<UnitKey, boolean> = {\n  days: false,\n  hours: true,\n  minutes: true,\n  seconds: true,\n}\n\nexport function activeUnitKeys(parts: CountdownParts): UnitKey[] {\n  return UNIT_KEYS.filter((key) => key === \"seconds\" || parts[key] !== 0)\n}\n\nexport type BoardSite = {\n  headline: string\n  name: string\n  units: readonly string[]\n}\n\nexport type BoardLayout = {\n  rows: string[]\n  cols: number\n  rowIds: string[]\n}\n\nexport const UNIT_SLOT_COUNT = UNIT_KEYS.length\n\nexport const TARGET_CELL_PX = 40\nexport const CELL_ASPECT = 4 / 3\nconst MAX_FRAME_COLS = 45\nconst MAX_FRAME_ROWS = 28\n\nexport function boardGapPx(viewportWidth: number, rem = 16): number {\n  return Math.min(0.3 * rem, Math.max(0.16 * rem, 0.005 * viewportWidth))\n}\n\nexport function boardNaturalSize(\n  cols: number,\n  rows: number,\n  gap: number\n): { width: number; height: number } {\n  return {\n    width: cols * TARGET_CELL_PX + Math.max(0, cols - 1) * gap,\n    height: rows * TARGET_CELL_PX * CELL_ASPECT + Math.max(0, rows - 1) * gap,\n  }\n}\n\nexport function fitBoardScale(\n  frameWidth: number,\n  frameHeight: number,\n  cols: number,\n  rows: number,\n  gap: number\n): number {\n  const natural = boardNaturalSize(cols, rows, gap)\n  return Math.min(\n    frameWidth / Math.max(1, natural.width),\n    frameHeight / Math.max(1, natural.height),\n    1\n  )\n}\n\nexport function fitBoardFrame(\n  width: number,\n  height: number,\n  minCols: number,\n  minRows: number,\n  gap: number\n): { cols: number; rows: number } {\n  if (width <= 0 || height <= 0) return { cols: minCols, rows: minRows }\n\n  const preferredCols = Math.floor((width + gap) / (TARGET_CELL_PX + gap))\n  if (preferredCols < minCols) {\n    return { cols: minCols, rows: minRows }\n  }\n\n  const cols = Math.min(MAX_FRAME_COLS, preferredCols)\n  const cellH = TARGET_CELL_PX * CELL_ASPECT\n  const rows = Math.min(\n    MAX_FRAME_ROWS,\n    Math.max(minRows, Math.floor((height + gap) / (cellH + gap)))\n  )\n\n  return { cols, rows }\n}\n\nexport function padBoard(\n  layout: BoardLayout,\n  targetCols: number,\n  targetRows: number\n): BoardLayout {\n  const cols = Math.max(layout.cols, targetCols)\n  const rowCount = Math.max(layout.rows.length, targetRows)\n  const extraX = cols - layout.cols\n  const left = Math.floor(extraX / 2)\n  const extraY = rowCount - layout.rows.length\n  const top = Math.floor(extraY / 2)\n  const blank = \" \".repeat(cols)\n  const padded = layout.rows.map((row) => {\n    const inner = row.padEnd(layout.cols, \" \").slice(0, layout.cols)\n    return `${\" \".repeat(left)}${inner}${\" \".repeat(cols - left - layout.cols)}`\n  })\n  const topIds = Array.from({ length: top }, (_, i) => `pad-top-${i}`)\n  const bottomIds = Array.from(\n    { length: rowCount - top - padded.length },\n    (_, i) => `pad-bottom-${i}`\n  )\n  return {\n    cols,\n    rows: [\n      ...Array.from({ length: top }, () => blank),\n      ...padded,\n      ...Array.from({ length: rowCount - top - padded.length }, () => blank),\n    ],\n    rowIds: [...topIds, ...layout.rowIds, ...bottomIds],\n  }\n}\n\nexport function centerIn(text: string, width: number): string {\n  const trimmed = text.slice(0, width)\n  const pad = width - trimmed.length\n  const left = Math.floor(pad / 2)\n  return `${\" \".repeat(left)}${trimmed}${\" \".repeat(pad - left)}`\n}\n\nexport function wrapToRows(text: string, cols: number, maxLines = Infinity): string[] {\n  const words = text.toUpperCase().split(/\\s+/).filter(Boolean)\n  const lines: string[] = []\n  let line = \"\"\n\n  for (const word of words) {\n    const next = line ? `${line} ${word}` : word\n    if (next.length > cols) {\n      if (line) lines.push(line)\n      line = word.length > cols ? word.slice(0, cols) : word\n    } else {\n      line = next\n    }\n  }\n  if (line) lines.push(line)\n  if (lines.length === 0) return [\"\"]\n\n  if (lines.length <= maxLines) return lines\n\n  const kept = lines.slice(0, maxLines)\n  const last = kept[maxLines - 1]!\n  kept[maxLines - 1] =\n    last.length >= cols ? `${last.slice(0, cols - 1)}…` : `${last}…`\n  return kept\n}\n\nexport function wrapBalanced(text: string, width: number, maxLines = Infinity): string[] {\n  const greedy = wrapToRows(text, width, maxLines)\n  if (greedy.length <= 1) return greedy\n\n  const words = text.toUpperCase().split(/\\s+/).filter(Boolean)\n  const minWidth = Math.max(\n    1,\n    ...words.map((word) => Math.min(word.length, width))\n  )\n  let lo = minWidth\n  let hi = width\n  while (lo < hi) {\n    const mid = Math.floor((lo + hi) / 2)\n    if (wrapToRows(text, mid, Infinity).length <= greedy.length) hi = mid\n    else lo = mid + 1\n  }\n  return wrapToRows(text, lo, maxLines)\n}\n\nexport function firstGapIndex(row: string): number {\n  const start = row.search(/\\S/)\n  if (start < 0) return Math.floor(row.length / 2)\n  const gap = row.indexOf(\" \", start)\n  return gap >= 0 ? gap : Math.floor(row.length / 2)\n}\n\nfunction splitPair(text: string): { left: string; right: string } {\n  const words = text.toUpperCase().split(/\\s+/).filter(Boolean)\n  if (words.length === 0) return { left: \"\", right: \"\" }\n  if (words.length === 1) return { left: words[0]!, right: \"\" }\n  return { left: words[0]!, right: words.slice(1).join(\" \") }\n}\n\nexport function placeAtAxis(\n  left: string,\n  right: string,\n  cols: number,\n  axis: number\n): string {\n  const cells = Array.from({ length: cols }, () => \" \")\n  const leftStart = axis - left.length\n  for (let i = 0; i < left.length; i++) {\n    const col = leftStart + i\n    if (col >= 0 && col < axis) cells[col] = left[i]!\n  }\n  for (let i = 0; i < right.length; i++) {\n    const col = axis + 1 + i\n    if (col >= 0 && col < cols) cells[col] = right[i]!\n  }\n  return cells.join(\"\")\n}\n\nexport function unitValue(key: UnitKey, parts: CountdownParts): string {\n  return UNIT_PADDED[key]\n    ? padDigits(parts[key], 2)\n    : String(Math.max(0, Math.floor(parts[key])))\n}\n\nexport type UnitOverride = { left?: string; right?: string }\n\nexport type LayoutBoardOptions = {\n  unitKeys?: readonly UnitKey[]\n  unitOverrides?: Partial<Record<UnitKey, UnitOverride>>\n}\n\nexport function layoutBoard(\n  site: BoardSite,\n  parts: CountdownParts,\n  cols: number = BOARD_COLS,\n  options?: LayoutBoardOptions\n): BoardLayout {\n  const width = cols\n  const blank = \" \".repeat(width)\n\n  const headlinePairs = wrapBalanced(site.headline, width, MAX_HEADING_LINES).map(\n    splitPair\n  )\n  const namePair = splitPair(site.name)\n  const keys = options?.unitKeys ?? activeUnitKeys(parts)\n  const unitPairs = keys.map((key) => {\n    const override = options?.unitOverrides?.[key]\n    const i = UNIT_KEYS.indexOf(key)\n    return {\n      key,\n      left: override?.left ?? unitValue(key, parts),\n      right: override?.right ?? (site.units[i] ?? \"\").toUpperCase(),\n    }\n  })\n\n  const padCount = Math.max(0, UNIT_SLOT_COUNT - unitPairs.length)\n  const slotPairs = [\n    ...unitPairs,\n    ...Array.from({ length: padCount }, () => ({\n      key: null as UnitKey | null,\n      left: \"\",\n      right: \"\",\n    })),\n  ]\n\n  const pairs = [\n    ...headlinePairs,\n    namePair,\n    ...slotPairs.map(({ left, right }) => ({ left, right })),\n  ]\n  const maxLeft = Math.max(1, ...pairs.map((pair) => pair.left.length))\n  const maxRight = Math.max(0, ...pairs.map((pair) => pair.right.length))\n  const contentWidth = Math.min(width, maxLeft + 1 + maxRight)\n  const leftPad = Math.max(0, Math.floor((width - contentWidth) / 2) - 1)\n  const axis = leftPad + maxLeft\n\n  const nameRightWidth = Math.max(1, width - axis - 1)\n  const nameRows = wrapToRows(namePair.right, nameRightWidth, MAX_NAME_LINES).map(\n    (line, i) => placeAtAxis(i === 0 ? namePair.left : \"\", line, width, axis)\n  )\n\n  const unitRows = slotPairs.map((pair) =>\n    pair.key === null\n      ? blank\n      : placeAtAxis(pair.left, pair.right, width, axis)\n  )\n\n  const unitRowIds = slotPairs.map((_, i) => `unit-slot-${i}`)\n\n  const rows = [\n    blank,\n    ...headlinePairs.map((pair) => placeAtAxis(pair.left, pair.right, width, axis)),\n    blank,\n    ...nameRows,\n    blank,\n    ...unitRows,\n    blank,\n  ]\n\n  const rowIds = [\n    \"spacer-top\",\n    ...headlinePairs.map((_, i) => `headline-${i}`),\n    \"spacer-after-headline\",\n    ...nameRows.map((_, i) => `name-${i}`),\n    \"spacer-before-units\",\n    ...unitRowIds,\n    \"spacer-bottom\",\n  ]\n\n  return { rows, cols: width, rowIds }\n}\n\nexport function buildBoardRows(\n  site: BoardSite,\n  parts: CountdownParts,\n  cols?: number\n): string[] {\n  return layoutBoard(site, parts, cols).rows\n}\n",
      "type": "registry:lib",
      "target": "lib/board.ts"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/lib/countdown.ts",
      "content": "export type CountdownParts = {\n  days: number\n  hours: number\n  minutes: number\n  seconds: number\n}\n\nconst MS_PER_SECOND = 1000\nconst MS_PER_MINUTE = 60 * MS_PER_SECOND\nconst MS_PER_HOUR = 60 * MS_PER_MINUTE\nconst MS_PER_DAY = 24 * MS_PER_HOUR\n\nexport function remainingParts(target: Date | string, now: Date): CountdownParts {\n  const end = typeof target === \"string\" ? new Date(target) : target\n  const diff = Math.max(0, end.getTime() - now.getTime())\n\n  const days = Math.floor(diff / MS_PER_DAY)\n  const hours = Math.floor((diff % MS_PER_DAY) / MS_PER_HOUR)\n  const minutes = Math.floor((diff % MS_PER_HOUR) / MS_PER_MINUTE)\n  const seconds = Math.floor((diff % MS_PER_MINUTE) / MS_PER_SECOND)\n\n  return { days, hours, minutes, seconds }\n}\n\nexport function padDigits(value: number, width = 2): string {\n  return String(Math.max(0, Math.floor(value))).padStart(width, \"0\")\n}\n",
      "type": "registry:lib",
      "target": "lib/countdown.ts"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/lib/site.ts",
      "content": "export const site = {\n  headline: \"Countdown Until\",\n  name: \"uiception 2.0\",\n  targetDate: \"2026-12-18T00:00:00-05:00\",\n  units: [\"Days\", \"Hours\", \"Minutes\", \"Seconds\"] as const,\n} as const\n",
      "type": "registry:lib",
      "target": "lib/site.ts"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/styles/solari.css",
      "content": ".solari-board {\n  --solari-ink: 6%;\n  --solari-empty-ink: 1.5%;\n  --solari-duration: 260ms;\n  --solari-paper: var(--secondary);\n  --solari-text: var(--foreground);\n\n  position: relative;\n  width: 100%;\n  max-width: 64rem;\n  margin-inline: auto;\n}\n\n.solari-board-fill {\n  max-width: none;\n  width: 100%;\n  height: 100%;\n  margin: 0;\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.solari-sizer {\n  position: relative;\n  flex: 0 0 auto;\n}\n\n.solari-sizer .solari-grid {\n  position: absolute;\n  top: 0;\n  left: 0;\n  transform-origin: top left;\n}\n\n.dark .solari-board {\n  --solari-ink: 10%;\n  --solari-empty-ink: 2%;\n  --solari-paper: var(--card);\n  --solari-text: var(--foreground);\n}\n\n.solari-defs {\n  position: absolute;\n  width: 0;\n  height: 0;\n  overflow: hidden;\n}\n\n.solari-grid {\n  position: relative;\n  display: grid;\n  gap: clamp(0.16rem, 0.5vw, 0.3rem);\n  perspective: 900px;\n}\n\n.solari-cell {\n  --cell-ink: var(--solari-ink);\n  --flap-top-ink: calc(var(--cell-ink) - 3%);\n  --flap-bottom-ink: calc(var(--cell-ink) + 6%);\n  --paper-mix: color-mix(\n    in oklch,\n    var(--foreground) var(--cell-ink),\n    var(--background)\n  );\n  --solari-well: color-mix(in oklch, var(--solari-paper) 58%, var(--paper-mix));\n  --solari-flap-top: color-mix(\n    in oklch,\n    var(--solari-paper) 70%,\n    color-mix(in oklch, var(--foreground) var(--flap-top-ink), var(--background))\n  );\n  --solari-flap-bottom: color-mix(\n    in oklch,\n    var(--solari-paper) 48%,\n    color-mix(in oklch, var(--foreground) var(--flap-bottom-ink), var(--background))\n  );\n  --solari-hinge: color-mix(in oklch, var(--foreground) 30%, var(--solari-paper));\n  --solari-tab: color-mix(in oklch, var(--foreground) 28%, var(--solari-paper));\n\n  position: relative;\n  aspect-ratio: 3 / 4;\n  border-radius: 0.16rem;\n  background: var(--solari-well);\n  box-shadow:\n    0.5px 0 1.5px color-mix(in oklch, var(--foreground) 18%, transparent) inset,\n    -0.5px 0 1.5px color-mix(in oklch, var(--foreground) 12%, transparent) inset,\n    0 1px 1px color-mix(in oklch, var(--foreground) 14%, transparent) inset,\n    0 0 0 1px color-mix(in oklch, var(--foreground) 7%, transparent) inset;\n  overflow: hidden;\n  font-family: var(--font-mono, ui-monospace, monospace);\n  font-weight: 600;\n  font-size: clamp(0.7rem, 2.8vw, 1.55rem);\n  line-height: 1;\n  letter-spacing: 0.01em;\n  color: var(--solari-text);\n  text-transform: uppercase;\n  user-select: none;\n}\n\n.solari-board-fill .solari-cell {\n  font-size: calc(var(--solari-cell-size, 2.5rem) * 0.52);\n}\n\n.solari-cell:nth-child(3n):not(.solari-cell-empty) {\n  --cell-ink: calc(var(--solari-ink) + 1.4%);\n}\n\n.solari-cell:nth-child(5n):not(.solari-cell-empty) {\n  --cell-ink: calc(var(--solari-ink) - 1.1%);\n}\n\n.solari-cell:nth-child(7n):not(.solari-cell-empty) {\n  --flap-top-ink: calc(var(--cell-ink) - 4.5%);\n}\n\n.solari-cell-empty {\n  --cell-ink: var(--solari-empty-ink);\n  --flap-top-ink: var(--cell-ink);\n  --flap-bottom-ink: var(--cell-ink);\n  --solari-hinge: color-mix(in oklch, var(--foreground) 6%, var(--background));\n  --solari-tab: transparent;\n  opacity: 0.4;\n  box-shadow: none;\n}\n\n.dark .solari-cell-empty {\n  opacity: 0.22;\n}\n\n.solari-cell::before,\n.solari-cell::after {\n  content: \"\";\n  position: absolute;\n  left: 50%;\n  z-index: 5;\n  width: 28%;\n  height: 8%;\n  transform: translateX(-50%);\n  background: var(--solari-tab);\n  border-radius: 0.05rem;\n  opacity: 0.5;\n  pointer-events: none;\n}\n\n.solari-cell::before {\n  top: 3%;\n}\n\n.solari-cell::after {\n  bottom: 3%;\n}\n\n.solari-cell-empty::before,\n.solari-cell-empty::after {\n  opacity: 0;\n}\n\n.solari-half {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  height: 50%;\n  overflow: hidden;\n  backface-visibility: hidden;\n  -webkit-backface-visibility: hidden;\n}\n\n.solari-half::before {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  z-index: 0;\n  background: #737373;\n  opacity: 0.08;\n  mix-blend-mode: overlay;\n  filter: url(#solari-paper-grain);\n  pointer-events: none;\n}\n\n.dark .solari-half::before {\n  opacity: 0.1;\n}\n\n.solari-cell-empty .solari-half::before {\n  opacity: 0.03;\n}\n\n.solari-half-top {\n  top: 0;\n  transform-origin: bottom center;\n  border-radius: 0.14rem 0.14rem 0 0;\n  background-color: var(--solari-flap-top);\n  background-image:\n    repeating-linear-gradient(\n      90deg,\n      color-mix(in oklch, var(--foreground) 4%, transparent) 0 1px,\n      transparent 1px 4px\n    ),\n    linear-gradient(\n      180deg,\n      color-mix(in oklch, var(--solari-paper) 55%, var(--solari-flap-top)) 0%,\n      var(--solari-flap-top) 42%,\n      color-mix(in oklch, var(--solari-flap-top) 78%, var(--solari-hinge)) 100%\n    );\n  box-shadow:\n    0 1.5px 0 color-mix(in oklch, var(--solari-paper) 75%, oklch(1 0 0)) inset,\n    0 -1px 0 color-mix(in oklch, var(--solari-paper) 55%, oklch(1 0 0)),\n    0 1px 0 var(--solari-hinge);\n  z-index: 2;\n}\n\n.solari-half-bottom {\n  bottom: 0;\n  transform-origin: top center;\n  border-radius: 0 0 0.14rem 0.14rem;\n  background-color: var(--solari-flap-bottom);\n  background-image:\n    repeating-linear-gradient(\n      90deg,\n      color-mix(in oklch, var(--foreground) 4%, transparent) 0 1px,\n      transparent 1px 4px\n    ),\n    linear-gradient(\n      180deg,\n      color-mix(in oklch, var(--solari-flap-bottom) 72%, var(--solari-hinge)) 0%,\n      var(--solari-flap-bottom) 28%,\n      color-mix(in oklch, var(--solari-flap-bottom) 88%, var(--solari-paper)) 100%\n    );\n  box-shadow:\n    0 3px 4px color-mix(in oklch, var(--foreground) 28%, transparent) inset,\n    0 3px 4px color-mix(in oklch, var(--foreground) 18%, transparent);\n  z-index: 1;\n}\n\n.solari-glyph {\n  position: absolute;\n  left: 0;\n  z-index: 1;\n  width: 100%;\n  height: 200%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-shadow:\n    0 0.6px 0 color-mix(in oklch, var(--foreground) 22%, transparent),\n    0 0 0.4px color-mix(in oklch, var(--foreground) 35%, transparent);\n}\n\n.solari-half-top .solari-glyph {\n  top: 0;\n}\n\n.solari-half-bottom .solari-glyph {\n  bottom: 0;\n}\n\n.solari-static-top {\n  z-index: 0;\n}\n\n.solari-static-bottom {\n  z-index: 0;\n}\n\n.solari-flip-top {\n  z-index: 3;\n  animation: solari-flip-top var(--solari-duration) ease-in both;\n}\n\n.solari-flip-bottom {\n  z-index: 3;\n  animation: solari-flip-bottom var(--solari-duration) ease-out both;\n}\n\n@keyframes solari-flip-top {\n  from {\n    transform: rotateX(0deg);\n  }\n  to {\n    transform: rotateX(-90deg);\n  }\n}\n\n@keyframes solari-flip-bottom {\n  from {\n    transform: rotateX(90deg);\n  }\n  to {\n    transform: rotateX(0deg);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .solari-board {\n    --solari-duration: 0ms;\n  }\n\n  .solari-flip-top,\n  .solari-flip-bottom {\n    animation: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/solari.css"
    },
    {
      "path": "registry/new-york/templates/landing-page-v3/.env.example",
      "content": "# Landing Page v3 needs no environment variables to run. Countdown target,\n# copy, and the solari-board content all live in the template's lib/ files.\n# Wire up your own form/CRM backend where the template marks the stub.\n",
      "type": "registry:file",
      "target": ".env.example"
    }
  ],
  "docs": "New Next.js project only (same flow as https://ui.shadcn.com/create):\n  npx shadcn@latest init --template next -y \"https://uiception.com/r/landing-page-v3.json\"\n\nThis scaffolds Next.js + shadcn, then installs the full site.\n\nEdit the headline, name, unit labels, and countdown targetDate in lib/site.ts — every field renders as flaps on a fixed 23-module board, nothing floats outside it. The headline is the alignment origin; the name and each countdown row (number, empty flap, label) hang off the same word-gap column (see lib/board.ts). Flaps mix from the host light/dark tokens; blank cells recede so the board needs no bezel. Anatomy in styles/solari.css.\n\nPreview at /view/landing-page-v3.",
  "categories": [
    "template",
    "landing-pages"
  ],
  "type": "registry:block"
}