{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-flap",
  "title": "Split-Flap Display",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 2 split flap variants: https://avaraui.com/view/split-flap. This item holds 2 of them as looks of one component (ivory, minimalist). Skip the question if the user already named one. An authentic, physics-driven Split-Flap Display component simulating retro-mechanical flap cards, 3D perspective flips, and synthesized click sounds with ivory and minimalist variants.",
  "dependencies": [
    "gsap"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/split-flap.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useState,\n  useEffect,\n  useRef,\n  useMemo,\n  forwardRef,\n  useImperativeHandle,\n} from \"react\";\nimport { gsap } from \"gsap\";\nimport { cn } from \"@/lib/utils\";\n\n// ============================================================================\n// CONSTANTS & TYPES\n// ============================================================================\nexport const SPLIT_FLAP_CHARSET = \" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,:!?-\";\nconst EDGE_THICKNESS = 3;\n\nexport interface SplitFlapProps {\n  /** Text to display or pipe-separated messages (e.g. \"DESIGN | SPLIT-FLAP | FUTURE UI\") */\n  text?: string;\n  /** Array of messages to cycle through */\n  messages?: string[];\n  /** Visual theme preset: \"ivory\" | \"minimalist\" | \"dark\" | \"light\" (default: \"ivory\") */\n  theme?: \"ivory\" | \"minimalist\" | \"dark\" | \"light\";\n  /** Also alias variant for backward compatibility */\n  variant?: \"ivory\" | \"minimalist\" | \"dark\" | \"light\";\n  /** Font size in pixels (default: 38) */\n  fontSize?: number;\n  /** Size preset: \"sm\" | \"md\" | \"lg\" (convenience shortcut for fontSize) */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Number of character columns. If omitted, automatically fits the longest message */\n  columns?: number;\n  /** Flip speed in milliseconds per unit (default: 60) */\n  speed?: number;\n  /** Stagger delay in milliseconds across columns (default: 80) */\n  stagger?: number;\n  /** Also alias staggerDelay */\n  staggerDelay?: number;\n  /** Dwell time in milliseconds each message stays stationary (default: 3000) */\n  dwell?: number;\n  /** Also alias interval for dwell time */\n  interval?: number;\n  /** Text color (default: \"#ff7657\" or theme accent) */\n  accentColor?: string;\n  /** Active theme color bridged from theme picker or custom hex */\n  color?: string;\n  /** Whether to play realistic mechanical click sound (default: false) */\n  sound?: boolean;\n  /** Whether to auto-cycle through messages (default: true) */\n  autoPlay?: boolean;\n  /** Custom class names */\n  className?: string;\n}\n\ninterface ThemeConfig {\n  plate: string;\n  plateBorder: string;\n  topFlap: string;\n  bottomFlap: string;\n  divider: string;\n  dividerGlow: string;\n  lightDarkness: number;\n  edgeColor: string;\n  edgeHighlight: string;\n  fontFamily: string;\n  defaultAccent: string;\n}\n\nconst SPLIT_FLAP_THEMES: Record<\n  string,\n  { light: ThemeConfig; dark: ThemeConfig }\n> = {\n  // 1. IVORY (Retro Solari departure board)\n  ivory: {\n    light: {\n      plate:\n        \"linear-gradient(180deg, #f8f7f4 0%, #f2efe9 48%, #eae5dc 50%, #ede9df 52%, #f4f1ea 100%)\",\n      plateBorder: \"1px solid #ded8cc\",\n      topFlap: \"linear-gradient(180deg, #f8f7f4 0%, #ede8de 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #e5dfd4 0%, #f4f1ea 100%)\",\n      divider: \"rgba(180, 170, 155, 0.45)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.8)\",\n      lightDarkness: 0.22,\n      edgeColor: \"#d5cec2\",\n      edgeHighlight: \"#ffffff\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ea583a\",\n    },\n    dark: {\n      plate:\n        \"linear-gradient(180deg, #23201d 0%, #181614 48%, #100f0e 50%, #181614 52%, #1f1d1a 100%)\",\n      plateBorder: \"1px solid rgba(255,255,255,0.1)\",\n      topFlap: \"linear-gradient(180deg, #262320 0%, #1a1816 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #131211 0%, #1e1c19 100%)\",\n      divider: \"rgba(0,0,0,0.85)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.06)\",\n      lightDarkness: 0.35,\n      edgeColor: \"#0a0a0a\",\n      edgeHighlight: \"#36322e\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ea583a\",\n    },\n  },\n\n  // 2. MINIMALIST (Clean frosted / slate glass cards)\n  minimalist: {\n    light: {\n      plate:\n        \"linear-gradient(180deg, #ffffff 0%, #f4f4f5 48%, #e4e4e7 50%, #f4f4f5 52%, #ffffff 100%)\",\n      plateBorder: \"1px solid rgba(228,228,231,0.9)\",\n      topFlap: \"linear-gradient(180deg, #ffffff 0%, #f4f4f5 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #f4f4f5 0%, #ffffff 100%)\",\n      divider: \"rgba(0,0,0,0.12)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.8)\",\n      lightDarkness: 0.2,\n      edgeColor: \"#e4e4e7\",\n      edgeHighlight: \"#ffffff\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#27272a\",\n    },\n    dark: {\n      plate:\n        \"linear-gradient(180deg, #222227 0%, #161619 48%, #0e0e10 50%, #161619 52%, #1e1e22 100%)\",\n      plateBorder: \"1px solid rgba(255,255,255,0.12)\",\n      topFlap: \"linear-gradient(180deg, #25252b 0%, #18181c 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #111114 0%, #1d1d21 100%)\",\n      divider: \"rgba(0,0,0,0.8)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.06)\",\n      lightDarkness: 0.32,\n      edgeColor: \"#09090b\",\n      edgeHighlight: \"#32323a\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#f4f4f5\",\n    },\n  },\n\n  // 3. DARK (Departure board dark)\n  dark: {\n    light: {\n      plate:\n        \"linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 48%, #111 50%, #1a1a1a 52%, #222 100%)\",\n      plateBorder: \"1px solid rgba(255,255,255,0.08)\",\n      topFlap: \"linear-gradient(180deg, #2a2a2a 0%, #1e1e1e 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #1a1a1a 0%, #222 100%)\",\n      divider: \"rgba(0,0,0,0.7)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.04)\",\n      lightDarkness: 0.35,\n      edgeColor: \"#0a0a0a\",\n      edgeHighlight: \"#333333\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ff7657\",\n    },\n    dark: {\n      plate:\n        \"linear-gradient(180deg, #2a2a2a 0%, #1a1a1a 48%, #111 50%, #1a1a1a 52%, #222 100%)\",\n      plateBorder: \"1px solid rgba(255,255,255,0.08)\",\n      topFlap: \"linear-gradient(180deg, #2a2a2a 0%, #1e1e1e 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #1a1a1a 0%, #222 100%)\",\n      divider: \"rgba(0,0,0,0.7)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.04)\",\n      lightDarkness: 0.35,\n      edgeColor: \"#0a0a0a\",\n      edgeHighlight: \"#333333\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ff7657\",\n    },\n  },\n\n  // 4. LIGHT (Departure board light)\n  light: {\n    light: {\n      plate:\n        \"linear-gradient(180deg, #f5f5f5 0%, #e8e8e8 48%, #ddd 50%, #e8e8e8 52%, #f0f0f0 100%)\",\n      plateBorder: \"1px solid rgba(0,0,0,0.1)\",\n      topFlap: \"linear-gradient(180deg, #f5f5f5 0%, #eee 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #e8e8e8 0%, #f0f0f0 100%)\",\n      divider: \"rgba(0,0,0,0.15)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.6)\",\n      lightDarkness: 0.2,\n      edgeColor: \"#bbb\",\n      edgeHighlight: \"#ddd\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ff7657\",\n    },\n    dark: {\n      plate:\n        \"linear-gradient(180deg, #f5f5f5 0%, #e8e8e8 48%, #ddd 50%, #e8e8e8 52%, #f0f0f0 100%)\",\n      plateBorder: \"1px solid rgba(0,0,0,0.1)\",\n      topFlap: \"linear-gradient(180deg, #f5f5f5 0%, #eee 100%)\",\n      bottomFlap: \"linear-gradient(180deg, #e8e8e8 0%, #f0f0f0 100%)\",\n      divider: \"rgba(0,0,0,0.15)\",\n      dividerGlow: \"0 1px 0 rgba(255,255,255,0.6)\",\n      lightDarkness: 0.2,\n      edgeColor: \"#bbb\",\n      edgeHighlight: \"#ddd\",\n      fontFamily: \"'Courier New', 'Consolas', 'Monaco', monospace\",\n      defaultAccent: \"#ff7657\",\n    },\n  },\n};\n\nfunction getSplitFlapTheme(\n  themeKey: string = \"ivory\",\n  isDark: boolean = false\n): ThemeConfig {\n  const preset =\n    SPLIT_FLAP_THEMES[themeKey] || SPLIT_FLAP_THEMES.ivory;\n  return isDark ? preset.dark : preset.light;\n}\n\n/** Hook to automatically detect system / document dark mode */\nfunction useIsDarkMode(): boolean {\n  const [isDark, setIsDark] = useState<boolean>(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    const check = () => {\n      const hasDarkClass = document.documentElement.classList.contains(\"dark\");\n      const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n      setIsDark(\n        hasDarkClass ||\n          (prefersDark && !document.documentElement.classList.contains(\"light\"))\n      );\n    };\n\n    check();\n\n    const observer = new MutationObserver(check);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const mql = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    mql.addEventListener(\"change\", check);\n\n    return () => {\n      observer.disconnect();\n      mql.removeEventListener(\"change\", check);\n    };\n  }, []);\n\n  return isDark;\n}\n\n// ============================================================================\n// AUDIO GENERATOR\n// ============================================================================\nclass SplitFlapAudio {\n  private ctx: AudioContext | null = null;\n\n  private init() {\n    if (!this.ctx && typeof window !== \"undefined\") {\n      const AudioCtx =\n        window.AudioContext ||\n        (window as unknown as { webkitAudioContext: typeof AudioContext })\n          .webkitAudioContext;\n      if (AudioCtx) {\n        this.ctx = new AudioCtx();\n      }\n    }\n    if (this.ctx && this.ctx.state === \"suspended\") {\n      this.ctx.resume().catch(() => {});\n    }\n  }\n\n  playClick(pitchVariance = 1) {\n    try {\n      this.init();\n      if (!this.ctx) return;\n\n      const now = this.ctx.currentTime;\n      const osc = this.ctx.createOscillator();\n      const gain = this.ctx.createGain();\n      const filter = this.ctx.createBiquadFilter();\n\n      osc.type = \"triangle\";\n      osc.frequency.setValueAtTime(320 * pitchVariance, now);\n      osc.frequency.exponentialRampToValueAtTime(35, now + 0.04);\n\n      filter.type = \"lowpass\";\n      filter.frequency.setValueAtTime(1400, now);\n      filter.frequency.exponentialRampToValueAtTime(100, now + 0.04);\n\n      gain.gain.setValueAtTime(0.001, now);\n      gain.gain.linearRampToValueAtTime(0.12, now + 0.004);\n      gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.04);\n\n      osc.connect(filter);\n      filter.connect(gain);\n      gain.connect(this.ctx.destination);\n\n      osc.start(now);\n      osc.stop(now + 0.04);\n    } catch {\n      // The click is a nicety: a blocked or unsupported AudioContext just stays silent.\n    }\n  }\n}\n\nconst audioPlayer = new SplitFlapAudio();\n\n// Dynamic shadow calculation\nfunction applyDynamicShadow(\n  shadowEl: HTMLDivElement | null,\n  progress: number,\n  lightDarkness: number\n) {\n  const sin = Math.sin(progress * Math.PI);\n  if (shadowEl) {\n    shadowEl.style.opacity = `${sin * lightDarkness}`;\n  }\n}\n\n// Center/Pad message helper\nfunction padMessage(msg: string, targetLen: number): string {\n  if (msg.length >= targetLen) return msg.slice(0, targetLen);\n  const leftPad = Math.floor((targetLen - msg.length) / 2);\n  return msg.padStart(msg.length + leftPad, \" \").padEnd(targetLen, \" \");\n}\n\n// Theme & Color resolution helpers\nconst SPLIT_FLAP_COLOR_PRESETS: Record<string, string> = {\n  lavender: \"#9b8afb\",\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n};\n\nfunction resolveColor(color?: string): string | null {\n  if (!color || color === \"default\" || color === \"primary\") return null;\n  return SPLIT_FLAP_COLOR_PRESETS[color] ?? color;\n}\n\n// ============================================================================\n// FLAP UNIT COMPONENT (FutureUI Architecture)\n// ============================================================================\nexport interface FlapUnitHandle {\n  close: (speed: number) => gsap.core.Timeline;\n  open: (newChar: string, speed: number) => gsap.core.Timeline;\n  kill: () => void;\n}\n\ninterface FlapUnitProps {\n  fontSize: number;\n  accentColor: string;\n  colIndex: number;\n  theme: ThemeConfig;\n  soundEnabled: boolean;\n}\n\nconst FlapUnit = forwardRef<FlapUnitHandle, FlapUnitProps>(\n  ({ fontSize, accentColor, colIndex, theme: d, soundEnabled }, ref) => {\n    const staticTopRef = useRef<HTMLDivElement>(null);\n    const staticBottomRef = useRef<HTMLDivElement>(null);\n    const topFlapRef = useRef<HTMLDivElement>(null);\n    const bottomFlapRef = useRef<HTMLDivElement>(null);\n    const shadowTopRef = useRef<HTMLDivElement>(null);\n    const shadowBottomRef = useRef<HTMLDivElement>(null);\n\n    const currentTimeline = useRef<gsap.core.Timeline | null>(null);\n\n    const unitHeight = fontSize * 1.6;\n    const unitWidth = fontSize * 1.1;\n    const halfHeight = unitHeight / 2;\n\n    const fontStyle = useMemo<React.CSSProperties>(\n      () => ({\n        fontFamily: d.fontFamily,\n        fontSize: `${fontSize}px`,\n        fontWeight: 700,\n        color: accentColor,\n        lineHeight: `${unitHeight}px`,\n        textAlign: \"center\",\n        width: \"100%\",\n        position: \"absolute\",\n        left: 0,\n        userSelect: \"none\",\n      }),\n      [fontSize, accentColor, unitHeight, d.fontFamily]\n    );\n\n    // Immediately synchronize color change across all flap DOM nodes\n    useEffect(() => {\n      if (staticTopRef.current) staticTopRef.current.style.color = accentColor;\n      if (staticBottomRef.current) staticBottomRef.current.style.color = accentColor;\n      if (topFlapRef.current) {\n        const textEl = topFlapRef.current.querySelector<HTMLElement>(\".flap-text\");\n        if (textEl) textEl.style.color = accentColor;\n      }\n      if (bottomFlapRef.current) {\n        const textEl = bottomFlapRef.current.querySelector<HTMLElement>(\".flap-text\");\n        if (textEl) textEl.style.color = accentColor;\n      }\n    }, [accentColor]);\n\n    const killActiveTimeline = () => {\n      currentTimeline.current?.kill();\n      currentTimeline.current = null;\n    };\n\n    const resetShadows = () => {\n      if (shadowTopRef.current) shadowTopRef.current.style.opacity = \"0\";\n      if (shadowBottomRef.current) shadowBottomRef.current.style.opacity = \"0\";\n    };\n\n    const animateFlapStep = (\n      timeline: gsap.core.Timeline,\n      targetEl: HTMLElement | null,\n      shadowEl: HTMLDivElement | null,\n      startProgressDir: number,\n      targetRotX: number,\n      duration: number,\n      ease: string,\n      position?: string | number\n    ) => {\n      if (!targetEl) return;\n      const proxy = { p: 0 };\n      // The handle is rebuilt every render, so `d` is the committed theme.\n      const { lightDarkness } = d;\n\n      timeline.to(\n        targetEl,\n        {\n          rotateX: targetRotX,\n          duration,\n          ease,\n        },\n        position\n      );\n\n      timeline.to(\n        proxy,\n        {\n          p: 1,\n          duration,\n          ease,\n          onUpdate: () => {\n            const progress =\n              startProgressDir === 0 ? proxy.p : 1 - proxy.p;\n            applyDynamicShadow(shadowEl, progress, lightDarkness);\n          },\n        },\n        \"<\"\n      );\n    };\n\n    useImperativeHandle(ref, () => ({\n      // FutureUI Shutter Close: Flaps close toward center slit\n      close(speed: number) {\n        killActiveTimeline();\n        const topEl = topFlapRef.current;\n        const bottomEl = bottomFlapRef.current;\n        if (!topEl || !bottomEl) return gsap.timeline();\n\n        const topText = topEl.querySelector(\".flap-text\");\n        const bottomText = bottomEl.querySelector(\".flap-text\");\n        const duration = Math.max(speed / 1000, 0.08) * 2;\n        const tl = gsap.timeline();\n\n        tl.call(() => {\n          if (topText) topText.textContent = \"\";\n          if (bottomText) bottomText.textContent = \"\";\n          resetShadows();\n        });\n\n        tl.set(topEl, {\n          rotateX: -90,\n          opacity: 1,\n          zIndex: 3,\n          transformOrigin: \"top center\",\n        });\n        tl.set(bottomEl, {\n          rotateX: 90,\n          opacity: 1,\n          zIndex: 3,\n          transformOrigin: \"bottom center\",\n        });\n\n        animateFlapStep(\n          tl,\n          topEl,\n          shadowTopRef.current,\n          -90,\n          0,\n          duration,\n          \"power3.out\"\n        );\n        animateFlapStep(\n          tl,\n          bottomEl,\n          shadowBottomRef.current,\n          90,\n          0,\n          duration,\n          \"power3.out\",\n          \"<0.05\"\n        );\n\n        currentTimeline.current = tl;\n        return tl;\n      },\n\n      // FutureUI Shutter Open: New character unfolds open from center slit\n      open(newChar: string, speed: number) {\n        killActiveTimeline();\n        const staticTop = staticTopRef.current;\n        const staticBottom = staticBottomRef.current;\n        const topEl = topFlapRef.current;\n        const bottomEl = bottomFlapRef.current;\n        if (!staticTop || !staticBottom || !topEl || !bottomEl) {\n          return gsap.timeline();\n        }\n\n        const duration = Math.max(speed / 1000, 0.08) * 2;\n\n        const tl = gsap.timeline({\n          onComplete: () => {\n            currentTimeline.current = null;\n          },\n        });\n\n        tl.call(() => {\n          staticTop.textContent = newChar;\n          staticBottom.textContent = newChar;\n          if (soundEnabled && newChar.trim() !== \"\") {\n            audioPlayer.playClick(0.95 + Math.random() * 0.15);\n          }\n        });\n\n        tl.set(topEl, {\n          rotateX: 0,\n          opacity: 1,\n          zIndex: 3,\n          transformOrigin: \"top center\",\n        });\n        tl.set(bottomEl, {\n          rotateX: 0,\n          opacity: 1,\n          zIndex: 3,\n          transformOrigin: \"bottom center\",\n        });\n\n        animateFlapStep(\n          tl,\n          topEl,\n          shadowTopRef.current,\n          0,\n          -90,\n          duration,\n          \"power2.in\"\n        );\n        animateFlapStep(\n          tl,\n          bottomEl,\n          shadowBottomRef.current,\n          0,\n          90,\n          duration,\n          \"power2.in\",\n          \"<0.05\"\n        );\n\n        tl.call(resetShadows);\n        tl.set(topEl, { opacity: 0, zIndex: 1 });\n        tl.set(bottomEl, { opacity: 0, zIndex: 1 });\n\n        currentTimeline.current = tl;\n        return tl;\n      },\n\n      kill() {\n        killActiveTimeline();\n        resetShadows();\n        if (topFlapRef.current) gsap.set(topFlapRef.current, { opacity: 0, zIndex: 1 });\n        if (bottomFlapRef.current) gsap.set(bottomFlapRef.current, { opacity: 0, zIndex: 1 });\n      },\n    }));\n\n    return (\n      <div\n        style={{\n          position: \"relative\",\n          width: unitWidth,\n          height: unitHeight,\n          perspective: 400,\n        }}\n        data-col={colIndex}\n      >\n        {/* 1. Background Plate (clean border, no heavy shadow) */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: 5,\n            background: d.plate,\n            border: d.plateBorder,\n          }}\n        />\n\n        {/* 2. Static Top Flap. Its character is written by `open()`; React only\n            renders the blank it starts with, so re-renders never overwrite it. */}\n        <div\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            width: \"100%\",\n            height: halfHeight,\n            overflow: \"hidden\",\n            borderRadius: \"5px 5px 0 0\",\n            zIndex: 1,\n          }}\n        >\n          <div ref={staticTopRef} style={{ ...fontStyle, top: 0 }}>\n            {\" \"}\n          </div>\n        </div>\n\n        {/* 3. Static Bottom Flap */}\n        <div\n          style={{\n            position: \"absolute\",\n            bottom: 0,\n            left: 0,\n            width: \"100%\",\n            height: halfHeight,\n            overflow: \"hidden\",\n            borderRadius: \"0 0 5px 5px\",\n            zIndex: 1,\n          }}\n        >\n          <div ref={staticBottomRef} style={{ ...fontStyle, bottom: 0 }}>\n            {\" \"}\n          </div>\n        </div>\n\n        {/* 4. Animating Top Leaf (with 3D thickness edge) */}\n        <div\n          ref={topFlapRef}\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            width: \"100%\",\n            height: halfHeight,\n            borderRadius: \"5px 5px 0 0\",\n            transformOrigin: \"bottom center\",\n            transformStyle: \"preserve-3d\",\n            willChange: \"transform\",\n            zIndex: 3,\n            opacity: 0,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              overflow: \"hidden\",\n              borderRadius: \"5px 5px 0 0\",\n              background: d.topFlap,\n              backfaceVisibility: \"hidden\",\n            }}\n          >\n            <div className=\"flap-text\" style={{ ...fontStyle, top: 0 }}>\n              {\" \"}\n            </div>\n          </div>\n          {/* Dynamic Light Shadow */}\n          <div\n            ref={shadowTopRef}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              borderRadius: \"5px 5px 0 0\",\n              background: \"#000\",\n              opacity: 0,\n              pointerEvents: \"none\",\n              willChange: \"opacity\",\n            }}\n          />\n          {/* 3D Physical Edge Thickness */}\n          <div\n            style={{\n              position: \"absolute\",\n              bottom: 0,\n              left: 0,\n              width: \"100%\",\n              height: EDGE_THICKNESS,\n              background: `linear-gradient(180deg, ${d.edgeHighlight} 0%, ${d.edgeColor} 100%)`,\n              transform: \"rotateX(-90deg)\",\n              transformOrigin: \"bottom center\",\n              backfaceVisibility: \"hidden\",\n            }}\n          />\n        </div>\n\n        {/* 5. Animating Bottom Leaf (with 3D thickness edge) */}\n        <div\n          ref={bottomFlapRef}\n          style={{\n            position: \"absolute\",\n            bottom: 0,\n            left: 0,\n            width: \"100%\",\n            height: halfHeight,\n            borderRadius: \"0 0 5px 5px\",\n            transformOrigin: \"top center\",\n            transformStyle: \"preserve-3d\",\n            willChange: \"transform\",\n            zIndex: 2,\n            opacity: 0,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              overflow: \"hidden\",\n              borderRadius: \"0 0 5px 5px\",\n              background: d.bottomFlap,\n              backfaceVisibility: \"hidden\",\n            }}\n          >\n            <div className=\"flap-text\" style={{ ...fontStyle, bottom: 0 }}>\n              {\" \"}\n            </div>\n          </div>\n          {/* Dynamic Light Shadow */}\n          <div\n            ref={shadowBottomRef}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              borderRadius: \"0 0 5px 5px\",\n              background: \"#000\",\n              opacity: 0,\n              pointerEvents: \"none\",\n              willChange: \"opacity\",\n            }}\n          />\n          {/* 3D Physical Edge Thickness */}\n          <div\n            style={{\n              position: \"absolute\",\n              top: 0,\n              left: 0,\n              width: \"100%\",\n              height: EDGE_THICKNESS,\n              background: `linear-gradient(0deg, ${d.edgeHighlight} 0%, ${d.edgeColor} 100%)`,\n              transform: \"rotateX(90deg)\",\n              transformOrigin: \"top center\",\n              backfaceVisibility: \"hidden\",\n            }}\n          />\n        </div>\n\n        {/* 6. Center Split Line with Subtle Glow */}\n        <div\n          style={{\n            position: \"absolute\",\n            top: halfHeight - 0.5,\n            left: 0,\n            width: \"100%\",\n            height: 1,\n            background: d.divider,\n            zIndex: 10,\n            boxShadow: d.dividerGlow,\n          }}\n        />\n      </div>\n    );\n  }\n);\nFlapUnit.displayName = \"FlapUnit\";\n\n// ============================================================================\n// MAIN SPLIT-FLAP DISPLAY CONTAINER COMPONENT\n// ============================================================================\nexport const SplitFlap: React.FC<SplitFlapProps> = ({\n  text = \"SPLIT-FLAP | MECHANICAL 3D | TIMELESS CRAFT\",\n  messages,\n  theme,\n  variant,\n  fontSize,\n  size = \"md\",\n  columns,\n  speed = 60,\n  stagger,\n  staggerDelay,\n  dwell,\n  interval,\n  accentColor,\n  color,\n  sound = false,\n  autoPlay = true,\n  className,\n}) => {\n  // Determine effective theme with automatic dark mode support\n  const isDarkMode = useIsDarkMode();\n  const selectedThemeKey = theme || variant || \"ivory\";\n  const activeTheme = getSplitFlapTheme(selectedThemeKey, isDarkMode);\n\n  // Font size calculation from preset or explicit prop\n  const computedFontSize = useMemo(() => {\n    if (fontSize && fontSize > 0) return fontSize;\n    switch (size) {\n      case \"sm\":\n        return 26;\n      case \"lg\":\n        return 48;\n      case \"md\":\n      default:\n        return 38;\n    }\n  }, [fontSize, size]);\n\n  const rawColor = color || accentColor;\n  const resolvedAccent = resolveColor(rawColor);\n  const effectiveAccent = resolvedAccent || activeTheme.defaultAccent;\n  const effectiveStagger = stagger ?? staggerDelay ?? 80;\n  const effectiveDwell = dwell ?? interval ?? 3000;\n\n  // Normalize message list\n  const messageList: string[] = useMemo(() => {\n    if (messages && messages.length > 0) {\n      return messages.map((m) => m.trim().toUpperCase()).filter(Boolean);\n    }\n    if (text) {\n      return text\n        .split(\"|\")\n        .map((m) => m.trim().toUpperCase())\n        .filter(Boolean);\n    }\n    return [\"SPLIT-FLAP\", \"RETRO MECHANICAL\", \"SOLARI BOARD\"];\n  }, [messages, text]);\n\n  // Compute number of columns needed\n  const totalColumns = useMemo(() => {\n    if (columns && columns > 0) return columns;\n    const maxLen = Math.max(\n      ...messageList.map((m) => m.trimEnd().length),\n      10\n    );\n    return maxLen;\n  }, [columns, messageList]);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const unitsRef = useRef<(FlapUnitHandle | null)[]>([]);\n  const currentIndexRef = useRef(0);\n\n  // The cycle reads the latest timings on each turn without restarting.\n  const speedRef = useRef(speed);\n  const staggerRef = useRef(effectiveStagger);\n  const dwellRef = useRef(effectiveDwell);\n  useEffect(() => {\n    speedRef.current = speed;\n    staggerRef.current = effectiveStagger;\n    dwellRef.current = effectiveDwell;\n  }, [speed, effectiveStagger, effectiveDwell]);\n\n  const getPaddedMessage = (index: number) =>\n    padMessage(messageList[index] ?? \"\", totalColumns);\n\n  // The board turns outside React, so its label follows it the same way.\n  const announce = (message: string | undefined) => {\n    containerRef.current?.setAttribute(\"aria-label\", `Split-flap display: ${message}`);\n  };\n\n  // FutureUI GSAP Split-Flap Animation Cycle\n  useEffect(() => {\n    if (typeof window === \"undefined\") return;\n\n    // Initialize units with first message\n    const initialMessage = getPaddedMessage(0);\n    announce(messageList[0]);\n    for (let col = 0; col < unitsRef.current.length; col++) {\n      const unit = unitsRef.current[col];\n      if (!unit) continue;\n      const ch = initialMessage[col] ?? \" \";\n      unit.open(ch, 0).progress(1).kill();\n    }\n\n    if (!autoPlay || messageList.length <= 1) return;\n\n    let isMounted = true;\n    const timeouts: ReturnType<typeof setTimeout>[] = [];\n\n    const scheduleTimeout = (cb: () => void, ms: number) => {\n      timeouts.push(\n        setTimeout(() => {\n          if (isMounted) cb();\n        }, ms)\n      );\n    };\n\n    // Check if 2nd variant: random / scattered flip order (\"elomelo kore flip\")\n    const isRandomStagger =\n      selectedThemeKey === \"minimalist\" || selectedThemeKey === \"dark\";\n\n    const runWaveTransition = () => {\n      const currentSpeed = speedRef.current;\n      const currentStagger = staggerRef.current;\n      const currentDwell = dwellRef.current;\n\n      const totalSpan = (unitsRef.current.length - 1) * (currentStagger / 1000);\n      const flapDuration = Math.max(currentSpeed / 1000, 0.08) * 2;\n      const stepDuration = totalSpan + flapDuration + 0.15;\n\n      // Generate delays: either scattered/random (\"elomelo\") or sequential cascade\n      const delays: number[] = unitsRef.current.map((_, i) =>\n        isRandomStagger\n          ? Math.random() * totalSpan\n          : (i / Math.max(1, unitsRef.current.length - 1)) * totalSpan\n      );\n\n      // 1. Shutter Close: Flaps close toward center slit\n      for (let i = 0; i < unitsRef.current.length; i++) {\n        const unit = unitsRef.current[i];\n        if (unit) {\n          unit.close(currentSpeed).delay(delays[i] ?? 0);\n        }\n      }\n\n      // 2. Advance index and Shutter Open into new characters\n      scheduleTimeout(() => {\n        currentIndexRef.current =\n          (currentIndexRef.current + 1) % messageList.length;\n        const nextMessage = getPaddedMessage(currentIndexRef.current);\n        announce(messageList[currentIndexRef.current]);\n\n        // Regenerate random delays for open phase if random mode\n        const openDelays: number[] = unitsRef.current.map((_, i) =>\n          isRandomStagger\n            ? Math.random() * totalSpan\n            : (i / Math.max(1, unitsRef.current.length - 1)) * totalSpan\n        );\n\n        for (let i = 0; i < unitsRef.current.length; i++) {\n          const unit = unitsRef.current[i];\n          if (unit) {\n            unit\n              .open(nextMessage[i] ?? \" \", currentSpeed)\n              .delay(openDelays[i] ?? 0);\n          }\n        }\n\n        // Wait for all flaps to finish opening + exact 3 seconds dwell time before next cycle!\n        scheduleTimeout(runWaveTransition, stepDuration * 1000 + currentDwell);\n      }, stepDuration * 1000);\n    };\n\n    // First transition starts after initial dwell time (3s)\n    scheduleTimeout(runWaveTransition, effectiveDwell);\n\n    return () => {\n      isMounted = false;\n      timeouts.forEach(clearTimeout);\n      for (const unit of unitsRef.current) {\n        unit?.kill();\n      }\n    };\n  }, [messageList, totalColumns, autoPlay, effectiveDwell, selectedThemeKey]);\n\n  return (\n    <div\n      className={cn(\n        \"w-full max-w-5xl mx-auto flex items-center justify-center py-4\",\n        className\n      )}\n    >\n      {/* -------------------------------------------------------- */}\n      {/* NO BIG PARENT BACKGROUND OR HEAVY SHADOWS                 */}\n      {/* Cards sit naturally on parent container's background      */}\n      {/* -------------------------------------------------------- */}\n      <div\n        ref={containerRef}\n        style={{\n          display: \"inline-flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          background: \"transparent\",\n          boxShadow: \"none\",\n          border: \"none\",\n          gap: Math.max(3, computedFontSize * 0.1),\n          flexWrap: \"wrap\",\n          maxWidth: \"100%\",\n        }}\n        role=\"status\"\n        aria-label={`Split-flap display: ${messageList[0]}`}\n        aria-live=\"polite\"\n      >\n        {Array.from({ length: totalColumns }, (_, colIdx) => (\n          <FlapUnit\n            key={`col-${colIdx}`}\n            ref={(handle) => {\n              unitsRef.current[colIdx] = handle;\n            }}\n            fontSize={computedFontSize}\n            accentColor={effectiveAccent}\n            colIndex={colIdx}\n            theme={activeTheme}\n            soundEnabled={sound}\n          />\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport default SplitFlap;\n",
      "type": "registry:component",
      "target": "components/custom/split-flap.tsx"
    }
  ],
  "docs": "Named exports: SplitFlap, SPLIT_FLAP_CHARSET. Default export: SplitFlap. Variants: ivory | minimalist. Features 3D mechanical rotateX animation, Web Audio API sound generator, and responsive size presets.",
  "categories": [
    "data-display",
    "media"
  ],
  "type": "registry:component"
}
