{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-flap",
  "type": "registry:ui",
  "title": "Split flap",
  "description": "Split-flap display that flips each character through the alphabet like a departure board, with a column ripple, tones, sizes and reduced motion support.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/split-flap.tsx",
      "type": "registry:ui",
      "target": "components/ui/split-flap.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * SplitFlap: a departure-board display. Each character flips through the\n * alphabet to its target, top half falling over the bottom like a real\n * Solari board, with a staggered start per column so a whole row ripples.\n * Screen readers get the final text once; the flipping is decoration.\n */\n\nconst CHARSET = \" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-'/·\"\n\ntype SplitFlapProps = {\n  value?: string\n  /** Pad or cut to this many cells. */\n  length?: number\n  /** Milliseconds per single flip. */\n  speed?: number\n  /** Extra delay per column, for the ripple. */\n  stagger?: number\n  size?: \"sm\" | \"default\" | \"lg\"\n  /** Colour of the characters. */\n  tone?: \"default\" | \"accent\" | \"success\" | \"warning\" | \"danger\"\n  className?: string\n}\n\nconst SIZE = {\n  sm: \"h-6 w-[17px] text-[13px] rounded-[3px]\",\n  default: \"h-8 w-[22px] text-[17px] rounded-[4px]\",\n  lg: \"h-12 w-8 text-[26px] rounded-[5px]\",\n}\nconst TONE = {\n  default: \"text-[oklch(0.96_0.01_90)]\",\n  accent: \"text-accent\",\n  success: \"text-[oklch(0.82_0.16_150)]\",\n  warning: \"text-[oklch(0.86_0.15_85)]\",\n  danger: \"text-[oklch(0.74_0.17_28)]\",\n}\n\nfunction idx(c: string) {\n  const i = CHARSET.indexOf(c.toUpperCase())\n  return i < 0 ? 0 : i\n}\n\nfunction Cell({ target, speed, delay, size, tone }: { target: string; speed: number; delay: number; size: keyof typeof SIZE; tone: keyof typeof TONE }) {\n  const reduce = useReducedMotion()\n  const [cur, setCur] = React.useState(\" \")\n  const [next, setNext] = React.useState(\" \")\n  const [flip, setFlip] = React.useState(0)\n  const curRef = React.useRef(\" \")\n\n  React.useEffect(() => {\n    const goal = CHARSET[idx(target)]\n    if (reduce) {\n      curRef.current = goal\n      setCur(goal)\n      setNext(goal)\n      return\n    }\n    let t: ReturnType<typeof setTimeout>\n    const step = () => {\n      const c = curRef.current\n      if (c === goal) return\n      const n = CHARSET[(idx(c) + 1) % CHARSET.length]\n      setNext(n)\n      setFlip((f) => f + 1)\n      t = setTimeout(() => {\n        curRef.current = n\n        setCur(n)\n        t = setTimeout(step, 8)\n      }, speed)\n    }\n    t = setTimeout(step, delay)\n    return () => clearTimeout(t)\n  }, [target, speed, delay, reduce])\n\n  const half = \"absolute inset-x-0 h-1/2 overflow-hidden bg-[linear-gradient(oklch(0.24_0.008_260),oklch(0.19_0.008_260))]\"\n  const glyph = (c: string, bottom?: boolean) => (\n    <span className={cn(\"absolute inset-x-0 flex h-[200%] items-center justify-center\", bottom ? \"-top-full\" : \"top-0\")}>{c}</span>\n  )\n  const animating = cur !== next\n  return (\n    <span className={cn(\"relative inline-block shrink-0 font-mono leading-none font-semibold [perspective:260px]\", SIZE[size], TONE[tone])} aria-hidden>\n      {/* static: next top, current bottom */}\n      <span className={cn(half, \"top-0 rounded-t-[inherit]\")}>{glyph(next)}</span>\n      <span className={cn(half, \"bottom-0 rounded-b-[inherit] bg-[linear-gradient(oklch(0.2_0.008_260),oklch(0.16_0.008_260))]\")}>{glyph(cur, true)}</span>\n      {animating ? (\n        <>\n          <span\n            key={`t${flip}`}\n            className={cn(half, \"top-0 origin-bottom rounded-t-[inherit] [backface-visibility:hidden]\")}\n            style={{ animation: `flap-top ${speed * 0.55}ms cubic-bezier(0.4,0,1,1) forwards` }}\n          >\n            {glyph(cur)}\n          </span>\n          <span\n            key={`b${flip}`}\n            className={cn(half, \"bottom-0 origin-top rounded-b-[inherit] bg-[linear-gradient(oklch(0.2_0.008_260),oklch(0.16_0.008_260))] [backface-visibility:hidden]\")}\n            style={{ animation: `flap-bottom ${speed * 0.45}ms cubic-bezier(0,0,0.3,1.4) ${speed * 0.55}ms both` }}\n          >\n            {glyph(next, true)}\n          </span>\n        </>\n      ) : null}\n      {/* hinge */}\n      <span className=\"absolute inset-x-0 top-1/2 z-10 h-px -translate-y-1/2 bg-black/70\" />\n      <span className=\"absolute inset-0 rounded-[inherit] shadow-[inset_0_1px_0_oklch(1_0_0/0.08),0_1px_2px_oklch(0_0_0/0.4)]\" />\n    </span>\n  )\n}\n\nfunction SplitFlap({ value = \"NOW BOARDING\", length, speed = 70, stagger = 28, size = \"default\", tone = \"default\", className }: SplitFlapProps) {\n  const text = length ? value.toUpperCase().padEnd(length, \" \").slice(0, length) : value.toUpperCase()\n  return (\n    <span data-slot=\"split-flap\" className={cn(\"inline-flex gap-[2px]\", className)}>\n      <span className=\"sr-only\">{value}</span>\n      {text.split(\"\").map((c, i) => (\n        <Cell key={i} target={c} speed={speed} delay={i * stagger} size={size} tone={tone} />\n      ))}\n      <style>{`@keyframes flap-top{from{transform:rotateX(0)}to{transform:rotateX(-90deg)}}@keyframes flap-bottom{from{transform:rotateX(90deg)}to{transform:rotateX(0)}}`}</style>\n    </span>\n  )\n}\n\nexport { SplitFlap }\nexport type { SplitFlapProps }\n"
    }
  ]
}
