{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "type-fossil",
  "title": "Type Fossil",
  "description": "A readable word carries layers of fine contours, revises itself, then compresses into a clean typographic imprint.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/type-fossil/index.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { Easing, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface TypeFossilProps {\n  text?: string;\n  /** Earlier versions, separated by | or newlines. Up to twelve drafts. */\n  drafts?: string;\n  layers?: number;\n  /** Thickness of the typographic slice, in reference pixels. */\n  depth?: number;\n  fontSize?: number;\n  fontWeight?: number | string;\n  color?: string;\n  accentColor?: string;\n  backgroundColor?: string;\n  speed?: number;\n  className?: string;\n}\n\nexport const typeFossilLength = 174;\nexport const typeFossilDefaultDrafts =\n  \"Idea | Maybe | Try | Almost | Again | Closer\";\nconst finite = (n: number, fallback: number) =>\n  Number.isFinite(n) ? n : fallback;\nconst clamp = (n: number) => Math.min(1, Math.max(0, n));\nconst mix = (a: number, b: number, p: number) => a + (b - a) * p;\nconst progress = (t: number, a: number, b: number) => clamp((t - a) / (b - a));\nconst ease = Easing.bezier(0.65, 0, 0.3, 1);\nconst out = Easing.bezier(0.16, 1, 0.3, 1);\n\nexport function parseFossilDrafts(drafts: string) {\n  return drafts\n    .split(/[|\\n\\r]+/)\n    .map((value) => value.trim().replace(/\\s+/g, \" \"))\n    .filter(Boolean)\n    .slice(0, 12);\n}\n\nfunction timing(draftCount: number) {\n  const count = Math.round(Math.max(0, Math.min(12, finite(draftCount, 6))));\n  const collapseStart = count === 0 ? 16 : 20 + (count - 1) * 18 + 16;\n  return { draftCount: count, collapseStart, settle: collapseStart + 48 };\n}\n\nexport function getTypeFossilDuration({\n  drafts = typeFossilDefaultDrafts,\n  speed = 1,\n}: Pick<TypeFossilProps, \"drafts\" | \"speed\"> = {}) {\n  const rate = Math.max(0, finite(speed, 1));\n  const { settle } = timing(parseFossilDrafts(drafts).length);\n  return rate === 0 ? 1 : Math.max(1, Math.ceil((settle + 34) / rate));\n}\n\n/** One readable revision at a time, followed by a single compression into print. */\nexport function getTypeFossilState(\n  frame: number,\n  {\n    layers = 16,\n    depth = 100,\n    draftCount = 6,\n  }: Pick<TypeFossilProps, \"layers\" | \"depth\"> & { draftCount?: number } = {},\n) {\n  const t = Math.max(0, finite(frame, 0));\n  const count = Math.round(Math.max(4, Math.min(24, finite(layers, 16))));\n  const thickness = Math.max(50, Math.min(150, finite(depth, 100)));\n  const schedule = timing(draftCount);\n  const collapse = ease(progress(t, schedule.collapseStart, schedule.settle));\n  const open = mix(0.55, 1, out(progress(t, 0, 16)));\n  const spread = open * (1 - collapse);\n  let from = 0;\n  let to = 0;\n  let change = 0;\n  for (let revision = 0; revision < schedule.draftCount; revision++) {\n    const start = 20 + revision * 18;\n    if (t < start) break;\n    if (t >= start + 8) {\n      from = revision + 1;\n      to = from;\n      change = 0;\n    } else {\n      from = revision;\n      to = revision + 1;\n      change = ease(progress(t, start, start + 8));\n      break;\n    }\n  }\n  return {\n    frame: t,\n    count,\n    thickness,\n    from,\n    to,\n    change,\n    draftCount: schedule.draftCount,\n    settle: schedule.settle,\n    collapse,\n    spread,\n    spanX: thickness * 1.35 * spread,\n    spanY: -thickness * 0.68 * spread,\n    skew: -10 * (1 - collapse),\n    ghostOpacity: 1 - ease(progress(t, schedule.settle - 18, schedule.settle)),\n  };\n}\n\n/** Bounded parallel contours, no perspective division or near-plane disappearance. */\nexport function getFossilLayerPose(\n  state: ReturnType<typeof getTypeFossilState>,\n  index: number,\n) {\n  const i = Math.round(\n    Math.max(0, Math.min(state.count - 1, finite(index, 0))),\n  );\n  const fraction = i / (state.count - 1);\n  return {\n    x: 640 + state.spanX / 2 - state.spanX * fraction,\n    y: 360 + state.spanY / 2 - state.spanY * fraction,\n    opacity: i === 0 ? 1 : state.ghostOpacity,\n    skew: state.skew,\n    fraction,\n  };\n}\n\nfunction fitSize(text: string, fontSize: number) {\n  const widthUnits = Array.from(text).reduce(\n    (sum, char) =>\n      sum +\n      (/\\s/.test(char)\n        ? 0.35\n        : /[ilI.,'!:;]/.test(char)\n          ? 0.4\n          : /[MW@ЖШЩЮ]/.test(char)\n            ? 1\n            : 0.8),\n    0,\n  );\n  return Math.min(fontSize, 720 / Math.max(1, widthUnits));\n}\n\nexport function TypeFossil({\n  text = \"Form\",\n  drafts = typeFossilDefaultDrafts,\n  layers = 16,\n  depth = 100,\n  fontSize = 220,\n  fontWeight = 600,\n  color = \"#302b26\",\n  accentColor = \"#a800b7\",\n  backgroundColor = \"#eeeae2\",\n  speed = 1,\n  className,\n}: TypeFossilProps) {\n  const id = useId().replace(/:/g, \"\");\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n  const finalText = text.trim().replace(/\\s+/g, \" \");\n  const revisions = [...parseFossilDrafts(drafts), finalText];\n  const state = getTypeFossilState(\n    ((frame * 30) / fps) * Math.max(0, finite(speed, 1)),\n    { layers, depth, draftCount: revisions.length - 1 },\n  );\n  const size = Math.max(24, Math.min(300, finite(fontSize, 220)));\n  const weight = Math.max(100, Math.min(900, finite(Number(fontWeight), 600)));\n  const wipeX = mix(-100, 1380, state.change);\n  const renderSlice = (copy: string) => {\n    const font = fitSize(copy, size);\n    return Array.from({ length: state.count }, (_, order) => {\n      const index = state.count - 1 - order;\n      const pose = getFossilLayerPose(state, index);\n      return (\n        <text\n          key={index}\n          x=\"0\"\n          y={font * 0.35}\n          textAnchor=\"middle\"\n          fontSize={font}\n          fontWeight={weight}\n          transform={`translate(${pose.x} ${pose.y}) skewX(${pose.skew})`}\n          fill={index === 0 ? color : \"none\"}\n          stroke={index === 0 ? \"none\" : accentColor}\n          strokeWidth={index === 0 ? 0 : 0.75}\n          strokeOpacity={0.18 + (1 - pose.fraction) * 0.38}\n          opacity={pose.opacity}\n          paintOrder=\"stroke fill\"\n        >\n          {copy}\n        </text>\n      );\n    });\n  };\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        backgroundColor,\n        fontFamily: \"Arial, Helvetica, sans-serif\",\n      }}\n    >\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox=\"0 0 1280 720\"\n        preserveAspectRatio=\"xMidYMid meet\"\n        role=\"img\"\n        aria-label={finalText || \"Empty type fossil\"}\n      >\n        <defs>\n          <clipPath id={`${id}-old`}>\n            <rect\n              x={wipeX}\n              y=\"-100\"\n              width={1480 - (wipeX + 100)}\n              height=\"920\"\n            />\n          </clipPath>\n          <clipPath id={`${id}-new`}>\n            <rect x=\"-100\" y=\"-100\" width={wipeX + 100} height=\"920\" />\n          </clipPath>\n        </defs>\n        {finalText ? (\n          state.from === state.to ? (\n            renderSlice(revisions[state.from])\n          ) : (\n            <>\n              <g clipPath={`url(#${id}-old)`}>\n                {renderSlice(revisions[state.from])}\n              </g>\n              <g clipPath={`url(#${id}-new)`}>\n                {renderSlice(revisions[state.to])}\n              </g>\n            </>\n          )\n        ) : null}\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/type-fossil.tsx"
    }
  ],
  "type": "registry:component"
}
