{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "period-drop",
  "title": "Period Drop",
  "description": "A word rises out of its line, then a colored dot falls from above the frame, bounces with squash and stretch, and settles as the period.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/period-drop/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { Easing, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nconst { fontFamily: INTER } = loadFont(\"normal\", {\n  weights: [\"100\", \"900\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PeriodDropProps {\n  text?: string;\n  fontSize?: number;\n  fontWeight?: number;\n  fontFamily?: string;\n  color?: string;\n  dotColor?: string;\n  dotSize?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport const periodDropDefaults = {\n  text: \"remocn\",\n  fontSize: 160,\n  fontWeight: 800,\n  fontFamily: INTER,\n  color: \"#fafafa\",\n  dotColor: \"#D97757\",\n  dotSize: 0.2,\n  speed: 1,\n} satisfies Required<Omit<PeriodDropProps, \"className\">>;\n\n/** Frame where the dot has come to rest, squash included. */\nexport const periodDropLength = 47;\n\n/** Timeline constants on the 30 fps clock. */\nexport const periodDropTiming = {\n  riseFrames: 12,\n  riseStagger: 2,\n  dropStart: 20,\n  fallFrames: 12,\n  restitution: 0.35,\n  bounces: 2,\n  /** Drop height as a share of the composition height. */\n  dropHeight: 0.62,\n  /** `scaleY` at full speed. */\n  stretch: 0.3,\n  /** `scaleY` lost at a full-speed impact. */\n  squash: 0.22,\n  /** Frames for an impact squash to decay by 1/e. */\n  recover: 1.1,\n} as const;\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst easeOutQuart = Easing.out(Easing.poly(4));\n\nfunction finite(value: number | undefined, fallback: number) {\n  return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\n/** Maps a composition frame onto the 30 fps timeline, scaled by `speed`. */\nexport function getPeriodDropTime(\n  frame: number,\n  options: { fps?: number; speed?: number } = {},\n) {\n  const fps = Math.max(1, finite(options.fps, 30));\n  const speed = Math.max(0, finite(options.speed, 1));\n  return (Math.max(0, finite(frame, 0)) * 30 * speed) / fps;\n}\n\n/** How far letter `index` still sits below its line, 1 → 0. */\nexport function getPeriodDropRise(t: number, index: number) {\n  const start = periodDropTiming.riseStagger * index;\n  return interpolate(t, [start, start + periodDropTiming.riseFrames], [1, 0], {\n    ...CLAMP,\n    easing: easeOutQuart,\n  });\n}\n\n/** Frames at which the dot touches the line: the landing, then each bounce. */\nexport function getPeriodDropImpacts() {\n  const { dropStart, fallFrames, restitution, bounces } = periodDropTiming;\n  const impacts = [dropStart + fallFrames];\n  for (let n = 1; n <= bounces; n++) {\n    impacts.push(impacts[n - 1] + 2 * restitution ** n * fallFrames);\n  }\n  return impacts;\n}\n\nexport interface PeriodDropDot {\n  visible: boolean;\n  /** Height above the rest point as a share of the drop, 0 at rest, −1 at the start. */\n  y: number;\n  scaleX: number;\n  scaleY: number;\n}\n\n/**\n * The dot's fall and bounces. Heights are shares of the drop height, time is\n * in 30 fps frames. Gravity is set by the fall: `g = 2 / fall²`, so the\n * landing speed is `2 / fall`. Each bounce leaves at `restitution` times the\n * previous speed, and the squash of each impact scales with it.\n */\nexport function getPeriodDropDot(t: number): PeriodDropDot {\n  const { dropStart, fallFrames, restitution, stretch, squash, recover } =\n    periodDropTiming;\n  if (t < dropStart) return { visible: false, y: -1, scaleX: 1, scaleY: 1 };\n  const g = 2 / (fallFrames * fallFrames);\n  const landing = g * fallFrames;\n  const impacts = getPeriodDropImpacts();\n\n  let y = 0;\n  let speed = 0;\n  let deform = 0;\n  if (t < impacts[0]) {\n    const u = t - dropStart;\n    y = -1 + 0.5 * g * u * u;\n    speed = g * u;\n    deform = speed / landing;\n  } else {\n    let n = impacts.length - 1;\n    while (n > 0 && t < impacts[n]) n--;\n    const tau = t - impacts[n];\n    const bounce = n + 1;\n    const launch =\n      bounce < impacts.length ? restitution ** bounce * landing : 0;\n    const flight = launch > 0 ? (2 * launch) / g : 0;\n    if (tau < flight) {\n      y = -(launch * tau - 0.5 * g * tau * tau);\n      speed = Math.abs(launch - g * tau);\n    }\n    const impact = restitution ** n;\n    const decay = Math.exp(-tau / recover);\n    deform = -impact * decay + (1 - decay) * (speed / landing);\n  }\n  // Snap the last imperceptible wobble so the dot rests exactly.\n  if (Math.abs(deform) < 0.005) deform = 0;\n\n  const scaleY =\n    deform >= 0 ? 1 + stretch * deform : 1 + squash * Math.max(-1, deform);\n  return { visible: true, y: Math.min(0, y), scaleX: 1 / scaleY, scaleY };\n}\n\nexport function PeriodDrop({\n  text = periodDropDefaults.text,\n  fontSize = periodDropDefaults.fontSize,\n  fontWeight = periodDropDefaults.fontWeight,\n  fontFamily = periodDropDefaults.fontFamily,\n  color = periodDropDefaults.color,\n  dotColor = periodDropDefaults.dotColor,\n  dotSize = periodDropDefaults.dotSize,\n  speed = periodDropDefaults.speed,\n  className,\n}: PeriodDropProps) {\n  const frame = useCurrentFrame();\n  const { fps, height } = useVideoConfig();\n  const unit = height / 720;\n  const t = getPeriodDropTime(frame, { fps, speed });\n  const dot = getPeriodDropDot(t);\n  const drop = periodDropTiming.dropHeight * height;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        overflow: \"hidden\",\n      }}\n    >\n      <div\n        style={{\n          fontFamily,\n          fontSize: fontSize * unit,\n          lineHeight: 1,\n          fontWeight,\n          fontVariationSettings: `\"wght\" ${fontWeight}`,\n          color,\n          whiteSpace: \"pre\",\n        }}\n      >\n        {Array.from(text).map((char, i) => (\n          <span\n            key={i}\n            style={{\n              display: \"inline-block\",\n              overflow: \"hidden\",\n              // Extend the clip below the line box so descenders survive.\n              paddingBottom: \"0.15em\",\n              marginBottom: \"-0.15em\",\n              verticalAlign: \"top\",\n            }}\n          >\n            <span\n              style={{\n                display: \"inline-block\",\n                transform: `translateY(${getPeriodDropRise(t, i) * 115}%)`,\n              }}\n            >\n              {char}\n            </span>\n          </span>\n        ))}\n        <span\n          style={{\n            display: \"inline-block\",\n            width: `${dotSize}em`,\n            height: `${dotSize}em`,\n            marginLeft: \"0.04em\",\n            borderRadius: \"50%\",\n            background: dotColor,\n            visibility: dot.visible ? \"visible\" : \"hidden\",\n            transformOrigin: \"50% 100%\",\n            transform: `translateY(${dot.y * drop}px) scale(${dot.scaleX}, ${dot.scaleY})`,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/period-drop.tsx"
    }
  ],
  "type": "registry:component"
}
