{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stripe-type",
  "title": "Stripe Type",
  "description": "A word cut into horizontal bands, as in the IBM logo, slides together from alternating sides and locks, then can pass on through.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/stripe-type/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\nimport { useEffect, useState } from \"react\";\nimport {\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: INTER } = loadFont(\"normal\", {\n  weights: [\"100\", \"900\"],\n  subsets: [\"latin\"],\n});\n\nexport type StripeTypeSettle = \"striped\" | \"solid\";\n\nexport interface StripeTypeProps {\n  text?: string;\n  fontSize?: number;\n  fontWeight?: number;\n  fontFamily?: string;\n  color?: string;\n  stripes?: number;\n  gap?: number;\n  settle?: StripeTypeSettle;\n  exit?: boolean;\n  hold?: number;\n  speed?: number;\n  className?: string;\n}\n\nexport const stripeTypeDefaults = {\n  text: \"Launch\",\n  fontSize: 180,\n  fontWeight: 800,\n  fontFamily: INTER,\n  color: \"#fafafa\",\n  stripes: 8,\n  gap: 0.35,\n  settle: \"striped\",\n  exit: true,\n  hold: 30,\n  speed: 1,\n} satisfies Required<Omit<StripeTypeProps, \"className\">>;\n\n/** Beats on the 30 fps clock. */\nexport const stripeTypeTiming = {\n  stagger: 2,\n  travel: 16,\n  close: 10,\n  /** Frames of empty screen after the exit in the preview. */\n  tail: 10,\n} as const;\n\n/** Ink box of Inter in a `line-height: 1` box, in em from the top, until measured. */\nconst INTER_INK = { top: 0.136, bottom: 0.864 + 0.24 };\nconst MEASURE_SIZE = 100;\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n// Expo-shaped curves that land exactly on 0 and 1 (Easing.exp stops 2⁻¹⁰ short).\nconst easeOutExpo = Easing.bezier(0.16, 1, 0.3, 1);\nconst easeInExpo = Easing.bezier(0.7, 0, 0.84, 0);\nconst easeInOutCubic = Easing.inOut(Easing.cubic);\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 getStripeTypeTime(\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\nfunction stripeCount(stripes: number | undefined) {\n  return Math.max(1, Math.round(finite(stripes, stripeTypeDefaults.stripes)));\n}\n\nexport interface StripeBand {\n  /** Top of the band as a share of the ink box height. */\n  top: number;\n  /** Bottom of the band as a share of the ink box height. */\n  bottom: number;\n}\n\n/**\n * Bands over the ink box. With `n` bands and gap share `g` the pitch is\n * `1 / (n − g)`: the first band starts at the top, the last ends at the\n * bottom.\n */\nexport function getStripeBands(stripes: number, gap: number): StripeBand[] {\n  const n = stripeCount(stripes);\n  const g = Math.min(0.95, Math.max(0, finite(gap, 0)));\n  const pitch = 1 / (n - g);\n  return Array.from({ length: n }, (_, k) => ({\n    top: k * pitch,\n    bottom: k * pitch + pitch * (1 - g),\n  }));\n}\n\nexport interface StripeTypeTimeline {\n  /** Frame where the last band has locked. */\n  entered: number;\n  /** Frame where the gaps have closed (equals `entered` when striped). */\n  settled: number;\n  exitStart: number;\n  exitEnd: number;\n  /** Frames the preview needs. */\n  duration: number;\n}\n\nexport function getStripeTypeTimeline(\n  options: Pick<StripeTypeProps, \"stripes\" | \"settle\" | \"exit\" | \"hold\"> = {},\n): StripeTypeTimeline {\n  const n = stripeCount(options.stripes);\n  const { stagger, travel, close, tail } = stripeTypeTiming;\n  const entered = stagger * (n - 1) + travel;\n  const settled = options.settle === \"solid\" ? entered + close : entered;\n  const hold = Math.max(0, finite(options.hold, stripeTypeDefaults.hold));\n  const exitStart = settled + hold;\n  const exitEnd = exitStart + stagger * (n - 1) + travel;\n  const exit = options.exit !== false;\n  return {\n    entered,\n    settled,\n    exitStart,\n    exitEnd,\n    duration: exit ? exitEnd + tail : settled + hold,\n  };\n}\n\n/** Preview length for the given options and speed. */\nexport function getStripeTypeDuration(\n  options: Pick<\n    StripeTypeProps,\n    \"stripes\" | \"settle\" | \"exit\" | \"hold\" | \"speed\"\n  > = {},\n) {\n  const speed = finite(options.speed, 1);\n  if (speed <= 0) return 1;\n  return Math.ceil(getStripeTypeTimeline(options).duration / speed);\n}\n\n/**\n * Horizontal offset of band `k` as a share of its travel distance: −1 or 1\n * off the frame, 0 locked. Even bands enter from the left, odd from the right,\n * and on the exit each band passes on through the other side.\n */\nexport function getStripeBandOffset(\n  t: number,\n  k: number,\n  options: Pick<StripeTypeProps, \"stripes\" | \"settle\" | \"exit\" | \"hold\"> = {},\n) {\n  const { stagger, travel } = stripeTypeTiming;\n  const timeline = getStripeTypeTimeline(options);\n  const side = k % 2 === 0 ? -1 : 1;\n  const inStart = stagger * k;\n  const enter = interpolate(t, [inStart, inStart + travel], [1, 0], {\n    ...CLAMP,\n    easing: easeOutExpo,\n  });\n  // `|| 0` folds −0 into 0 for a locked band.\n  if (options.exit === false) return side * enter || 0;\n  const outStart = timeline.exitStart + stagger * k;\n  const leave = interpolate(t, [outStart, outStart + travel], [0, 1], {\n    ...CLAMP,\n    easing: easeInExpo,\n  });\n  return side * enter - side * leave || 0;\n}\n\n/** Gap share over time: the set gap, closing to 0 when `settle` is solid. */\nexport function getStripeGap(\n  t: number,\n  gap: number,\n  options: Pick<StripeTypeProps, \"stripes\" | \"settle\"> = {},\n) {\n  if (options.settle !== \"solid\") return gap;\n  const { entered, settled } = getStripeTypeTimeline(options);\n  return interpolate(t, [entered, settled], [gap, 0], {\n    ...CLAMP,\n    easing: easeInOutCubic,\n  });\n}\n\n/** Ink box of the word in its `line-height: 1` box, and its width, in em. */\nfunction useInkBox(text: string, font: string) {\n  const [ink, setInk] = useState<{\n    top: number;\n    bottom: number;\n    width: number;\n  } | null>(null);\n\n  useEffect(() => {\n    const handle = delayRender(\"stripe-type: measuring the word\");\n    let released = false;\n    const release = () => {\n      if (released) return;\n      released = true;\n      continueRender(handle);\n    };\n    const measure = () => {\n      if (released) return;\n      const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n      if (ctx) {\n        ctx.font = font;\n        const m = ctx.measureText(text);\n        const ascent = m.fontBoundingBoxAscent / MEASURE_SIZE;\n        const descent = m.fontBoundingBoxDescent / MEASURE_SIZE;\n        const baseline = (1 - (ascent + descent)) / 2 + ascent;\n        if (ascent > 0) {\n          setInk({\n            top: baseline - m.actualBoundingBoxAscent / MEASURE_SIZE,\n            bottom: baseline + m.actualBoundingBoxDescent / MEASURE_SIZE,\n            width: m.width / MEASURE_SIZE,\n          });\n        }\n      }\n      release();\n    };\n    document.fonts.load(font, text).then(measure, measure);\n    return release;\n  }, [text, font]);\n\n  return ink;\n}\n\nexport function StripeType({\n  text = stripeTypeDefaults.text,\n  fontSize = stripeTypeDefaults.fontSize,\n  fontWeight = stripeTypeDefaults.fontWeight,\n  fontFamily = stripeTypeDefaults.fontFamily,\n  color = stripeTypeDefaults.color,\n  stripes = stripeTypeDefaults.stripes,\n  gap = stripeTypeDefaults.gap,\n  settle = stripeTypeDefaults.settle,\n  exit = stripeTypeDefaults.exit,\n  hold = stripeTypeDefaults.hold,\n  speed = stripeTypeDefaults.speed,\n  className,\n}: StripeTypeProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const unit = height / 720;\n  const size = fontSize * unit;\n  const t = getStripeTypeTime(frame, { fps, speed });\n  const measured = useInkBox(\n    text,\n    `${fontWeight} ${MEASURE_SIZE}px ${fontFamily}`,\n  );\n  const ink = measured ?? {\n    ...INTER_INK,\n    width: 0.6 * Array.from(text).length,\n  };\n  const options = { stripes, settle, exit, hold };\n  const bands = getStripeBands(stripes, getStripeGap(t, gap, options));\n  const inkHeight = ink.bottom - ink.top;\n  // Far enough that a band starting on either side is fully off the frame.\n  const travel = (width + ink.width * size) / 2 + 0.2 * size;\n\n  const type = {\n    fontFamily,\n    fontSize: size,\n    lineHeight: 1,\n    fontWeight,\n    fontVariationSettings: `\"wght\" ${fontWeight}`,\n    color,\n    whiteSpace: \"pre\",\n  } as const;\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 style={{ position: \"relative\" }}>\n        <div aria-hidden style={{ ...type, visibility: \"hidden\" }}>\n          {text}\n        </div>\n        {bands.map((band, k) => {\n          const top = (ink.top + band.top * inkHeight) * 100;\n          // Each band reaches a pixel into the next gap, so closed gaps leave\n          // no antialiased seam between bands.\n          const bleed = k < bands.length - 1 ? Math.max(1, unit) / size : 0;\n          const bottom =\n            (1 - (ink.top + band.bottom * inkHeight) - bleed) * 100;\n          const offset = getStripeBandOffset(t, k, options);\n          return (\n            <div\n              key={k}\n              aria-hidden={k > 0}\n              style={{\n                ...type,\n                position: \"absolute\",\n                inset: 0,\n                clipPath: `inset(${top}% -1% ${bottom}% -1%)`,\n                transform: `translateX(${offset * travel}px)`,\n              }}\n            >\n              {text}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/stripe-type.tsx"
    }
  ],
  "type": "registry:component"
}
