{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "persona",
  "type": "registry:component",
  "title": "Persona",
  "description": "Native monochrome waveform, fluid orb, and ring visuals with five conversational states, live intensity, and a custom renderer.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/persona.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { useMergedRefs } from \"./merge-refs\";\n\nexport type PersonaState = \"idle\" | \"listening\" | \"thinking\" | \"speaking\" | \"asleep\";\nexport type PersonaVariant = \"waveform\" | \"orb\" | \"rings\";\nexport interface PersonaVisualContext {\n  state: PersonaState;\n  size: number;\n  /** Normalized input/output energy, or the state's representative intensity. */\n  intensity: number;\n  /** Whether motion is allowed by visibility, user preferences and paused/state. */\n  animated: boolean;\n}\nexport interface PersonaProps extends React.HTMLAttributes<HTMLDivElement> {\n  state?: PersonaState;\n  variant?: PersonaVariant;\n  size?: number;\n  label?: string;\n  /** Input/output energy in the range 0–1. Asleep always settles to zero. */\n  intensity?: number;\n  /** Stop motion without changing the conversational state. */\n  paused?: boolean;\n  /** Called initially and whenever the effective motion policy changes. */\n  onMotionChange?: (animated: boolean) => void;\n  /** A custom decorative visual. The application owns its rendering lifecycle. */\n  children?: React.ReactNode;\n  /** Custom visuals should honor animated and release their own rendering resources. */\n  renderVisual?: (context: PersonaVisualContext) => React.ReactNode;\n}\nconst breathe = stylex.keyframes({ \"0%, 100%\": { transform: \"scaleY(0.4)\" }, \"50%\": { transform: \"scaleY(1)\" } });\nconst flow = stylex.keyframes({\n  \"0%, 100%\": { borderRadius: \"44% 56% 61% 39% / 47% 42% 58% 53%\", transform: \"rotate(-8deg)\" },\n  \"50%\": { borderRadius: \"57% 43% 40% 60% / 42% 58% 42% 58%\", transform: \"rotate(8deg)\" },\n});\nconst resonate = stylex.keyframes({ \"0%, 100%\": { transform: \"scale(0.88)\", opacity: 0.5 }, \"50%\": { transform: \"scale(1)\", opacity: 1 } });\nconst styles = stylex.create({\n  root: {\n    display: \"inline-grid\", placeItems: \"center\", flexShrink: 0, verticalAlign: \"middle\", color: vlak.ink,\n    overflow: \"hidden\", borderRadius: \"50%\", backgroundColor: { default: vlak.controlFill, [mq.forcedColors]: \"Canvas\" },\n    \"--rs-persona-play-state\": \"paused\", \"--rs-persona-wave-gap\": \"8%\", \"--rs-persona-ring-stretch\": \"1\", \"--rs-persona-wave-height\": \"58%\", \"--rs-persona-visual-scale\": \"0.88\",\n  },\n  running: { \"--rs-persona-play-state\": \"running\" },\n  listening: { \"--rs-persona-wave-gap\": \"10%\" },\n  thinking: { \"--rs-persona-wave-gap\": \"4%\", \"--rs-persona-ring-stretch\": \"0.86\" },\n  speaking: { \"--rs-persona-wave-gap\": \"6%\", \"--rs-persona-ring-stretch\": \"0.96\" },\n  asleep: { \"--rs-persona-play-state\": \"paused\" },\n  visual: { display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"var(--rs-persona-wave-gap)\", width: \"60%\", height: \"50%\", transitionProperty: \"gap\", transitionDuration: { default: \"450ms\", [mq.reduce]: \"0ms\", [mq.forcedColors]: \"0ms\" }, transitionTimingFunction: \"ease-in-out\" },\n  bar: {\n    width: \"12%\", height: \"var(--rs-persona-wave-height, 58%)\", backgroundColor: { default: \"currentColor\", [mq.forcedColors]: \"CanvasText\" }, borderRadius: vlak.radiusSm,\n    transformOrigin: \"center\", animationName: { default: breathe, [mq.reduce]: \"none\", [mq.forcedColors]: \"none\" }, animationDuration: \"2.4s\", animationTimingFunction: \"ease-in-out\", animationIterationCount: \"infinite\", animationPlayState: \"var(--rs-persona-play-state)\",\n    transitionProperty: \"height\", transitionDuration: { default: \"450ms\", [mq.reduce]: \"0ms\", [mq.forcedColors]: \"0ms\" }, transitionTimingFunction: \"ease-in-out\",\n  },\n  orb: { display: \"grid\", placeItems: \"center\", width: \"74%\", height: \"74%\", transform: \"scale(var(--rs-persona-visual-scale, 0.88))\", transitionProperty: \"transform\", transitionDuration: { default: \"600ms\", [mq.reduce]: \"0ms\", [mq.forcedColors]: \"0ms\" }, transitionTimingFunction: \"ease-in-out\" },\n  orbSurface: {\n    display: \"block\", width: \"100%\", height: \"100%\", borderRadius: \"44% 56% 61% 39% / 47% 42% 58% 53%\",\n    backgroundImage: { default: `radial-gradient(circle at 32% 26%, ${vlak.paper}, ${vlak.gray} 36%, ${vlak.ink} 78%)`, [mq.forcedColors]: \"none\" }, backgroundColor: { default: vlak.ink, [mq.forcedColors]: \"CanvasText\" },\n    animationName: { default: flow, [mq.reduce]: \"none\", [mq.forcedColors]: \"none\" }, animationDuration: \"7.6s\", animationTimingFunction: \"ease-in-out\", animationIterationCount: \"infinite\", animationPlayState: \"var(--rs-persona-play-state)\",\n  },\n  rings: { display: \"grid\", placeItems: \"center\", width: \"82%\", height: \"82%\", transform: \"scale(var(--rs-persona-visual-scale, 0.88)) scaleY(var(--rs-persona-ring-stretch))\", transitionProperty: \"transform\", transitionDuration: { default: \"600ms\", [mq.reduce]: \"0ms\", [mq.forcedColors]: \"0ms\" }, transitionTimingFunction: \"ease-in-out\" },\n  ring: {\n    display: \"block\", gridArea: \"1 / 1\", boxSizing: \"border-box\", borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: { default: \"currentColor\", [mq.forcedColors]: \"CanvasText\" }, borderRadius: \"50%\",\n    animationName: { default: resonate, [mq.reduce]: \"none\", [mq.forcedColors]: \"none\" }, animationDuration: \"3.6s\", animationTimingFunction: \"ease-in-out\", animationIterationCount: \"infinite\", animationPlayState: \"var(--rs-persona-play-state)\",\n  },\n});\nconst stateIntensity: Record<PersonaState, number> = { idle: 0.25, listening: 0.65, thinking: 0.45, speaking: 0.85, asleep: 0 };\n\n/** Three native monochrome visuals share five conversational states and an optional custom renderer. */\nexport const Persona = React.forwardRef<HTMLDivElement, PersonaProps>(function Persona({ state = \"idle\", variant = \"waveform\", size = 48, label, intensity, paused = false, onMotionChange, children, renderVisual, className, style, ...props }, ref) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const mergedRef = useMergedRefs(rootRef, ref);\n  const [canAnimate, setCanAnimate] = React.useState(false);\n  React.useEffect(() => {\n    let mounted = true;\n    let intersecting = typeof IntersectionObserver !== \"function\";\n    const reduced = typeof window.matchMedia === \"function\" ? window.matchMedia(\"(prefers-reduced-motion: reduce)\") : null;\n    const forced = typeof window.matchMedia === \"function\" ? window.matchMedia(\"(forced-colors: active)\") : null;\n    const update = () => { if (mounted) setCanAnimate(intersecting && document.visibilityState !== \"hidden\" && !reduced?.matches && !forced?.matches); };\n    const observer = typeof IntersectionObserver === \"function\" ? new IntersectionObserver(entries => { intersecting = entries[0]?.isIntersecting ?? false; update(); }) : null;\n    if (rootRef.current) observer?.observe(rootRef.current);\n    document.addEventListener(\"visibilitychange\", update);\n    reduced?.addEventListener(\"change\", update); forced?.addEventListener(\"change\", update); update();\n    return () => {\n      mounted = false; observer?.disconnect(); document.removeEventListener(\"visibilitychange\", update);\n      reduced?.removeEventListener(\"change\", update); forced?.removeEventListener(\"change\", update);\n    };\n  }, []);\n  const animated = canAnimate && !paused && state !== \"asleep\";\n  const motionCallback = React.useRef(onMotionChange);\n  React.useEffect(() => { motionCallback.current = onMotionChange; }, [onMotionChange]);\n  React.useEffect(() => { motionCallback.current?.(animated); }, [animated]);\n  const diameter = Number.isFinite(size) ? Math.max(1, size) : 48;\n  const energy = state === \"asleep\" ? 0 : typeof intensity === \"number\" && Number.isFinite(intensity) ? Math.max(0, Math.min(1, intensity)) : stateIntensity[state];\n  const root = rs([\"rs-persona\", className, animated && \"rs-persona-running\", state === \"listening\" && \"rs-persona-listening\", state === \"thinking\" && \"rs-persona-thinking\", state === \"speaking\" && \"rs-persona-speaking\", state === \"asleep\" && \"rs-persona-asleep\"], styles.root, animated && styles.running, state === \"listening\" && styles.listening, state === \"thinking\" && styles.thinking, state === \"speaking\" && styles.speaking, state === \"asleep\" && styles.asleep);\n  const visual = rs([\"rs-persona-visual\"], styles.visual);\n  const bar = rs([\"rs-persona-bar\"], styles.bar);\n  const orb = rs([\"rs-persona-orb\"], styles.orb);\n  const orbSurface = rs([\"rs-persona-orb-surface\"], styles.orbSurface);\n  const rings = rs([\"rs-persona-rings\"], styles.rings);\n  const ring = rs([\"rs-persona-ring\"], styles.ring);\n  const builtIn = variant === \"orb\" ? <span {...orb} aria-hidden=\"true\"><span {...orbSurface} /></span> : variant === \"rings\" ? <span {...rings} aria-hidden=\"true\">{[0, 1, 2].map(index => <span key={index} {...ring} style={{ ...ring.style, width: `${44 + index * 26}%`, height: `${44 + index * 26}%`, animationDelay: `${index * -0.6}s` }} />)}</span> : <span {...visual} aria-hidden=\"true\">{[0, 1, 2, 3].map(index => <span key={index} {...bar} style={{ ...bar.style, animationDelay: `${index * -0.32}s` }} />)}</span>;\n  const semantics: React.HTMLAttributes<HTMLDivElement> = label ? { role: \"img\", \"aria-label\": `${label}: ${state}` } : { \"aria-hidden\": true };\n  return <div {...props} {...semantics} ref={mergedRef} data-state={state} data-variant={renderVisual || children != null ? \"custom\" : variant} data-animated={animated} className={root.className} style={{ ...root.style, width: diameter, height: diameter, \"--rs-persona-wave-height\": `${16 + energy * 84}%`, \"--rs-persona-visual-scale\": 0.72 + energy * 0.28, ...style } as React.CSSProperties}>\n    {renderVisual ? renderVisual({ state, size: diameter, intensity: energy, animated }) : children ?? builtIn}\n  </div>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/persona.tsx"
    },
    {
      "path": "vlak/styles/persona.css",
      "content": "/* ── persona: generated from packages/react/src/components/persona.tsx ── */\n@keyframes rs-anim-breathe{0%, 100%{transform:scaleY(0.4)}50%{transform:scaleY(1)}}\n@keyframes rs-anim-flow{0%, 100%{border-radius:44% 56% 61% 39% / 47% 42% 58% 53%;transform:rotate(-8deg)}50%{border-radius:57% 43% 40% 60% / 42% 58% 42% 58%;transform:rotate(8deg)}}\n@keyframes rs-anim-resonate{0%, 100%{transform:scale(0.88);opacity:0.5}50%{transform:scale(1);opacity:1}}\n.rs-persona{display:inline-grid;place-items:center;flex-shrink:0;vertical-align:middle;color:var(--text);overflow:hidden;border-radius:50%;background-color:var(--control-fill);--rs-persona-play-state:paused;--rs-persona-wave-gap:8%;--rs-persona-ring-stretch:1;--rs-persona-wave-height:58%;--rs-persona-visual-scale:0.88}\n@media (forced-colors: active){.rs-persona{background-color:Canvas}}\n.rs-persona-running{--rs-persona-play-state:running}\n.rs-persona-listening{--rs-persona-wave-gap:10%}\n.rs-persona-thinking{--rs-persona-wave-gap:4%;--rs-persona-ring-stretch:0.86}\n.rs-persona-speaking{--rs-persona-wave-gap:6%;--rs-persona-ring-stretch:0.96}\n.rs-persona-asleep{--rs-persona-play-state:paused}\n.rs-persona-visual{display:flex;align-items:center;justify-content:center;gap:var(--rs-persona-wave-gap);width:60%;height:50%;transition-property:gap;transition-duration:450ms;transition-timing-function:ease-in-out}\n@media (prefers-reduced-motion: reduce){.rs-persona-visual{transition-duration:0ms}}\n@media (forced-colors: active){.rs-persona-visual{transition-duration:0ms}}\n.rs-persona-bar{width:12%;height:var(--rs-persona-wave-height, 58%);background-color:currentColor;border-radius:var(--radius-sm);transform-origin:center;animation-name:rs-anim-breathe;animation-duration:2.4s;animation-timing-function:ease-in-out;animation-iteration-count:infinite;animation-play-state:var(--rs-persona-play-state);transition-property:height;transition-duration:450ms;transition-timing-function:ease-in-out}\n@media (forced-colors: active){.rs-persona-bar{background-color:CanvasText;animation-name:none;transition-duration:0ms}}\n@media (prefers-reduced-motion: reduce){.rs-persona-bar{animation-name:none;transition-duration:0ms}}\n.rs-persona-orb{display:grid;place-items:center;width:74%;height:74%;transform:scale(var(--rs-persona-visual-scale, 0.88));transition-property:transform;transition-duration:600ms;transition-timing-function:ease-in-out}\n@media (prefers-reduced-motion: reduce){.rs-persona-orb{transition-duration:0ms}}\n@media (forced-colors: active){.rs-persona-orb{transition-duration:0ms}}\n.rs-persona-orb-surface{display:block;width:100%;height:100%;border-radius:44% 56% 61% 39% / 47% 42% 58% 53%;background-image:radial-gradient(circle at 32% 26%, var(--bg), var(--text-secondary) 36%, var(--text) 78%);background-color:var(--text);animation-name:rs-anim-flow;animation-duration:7.6s;animation-timing-function:ease-in-out;animation-iteration-count:infinite;animation-play-state:var(--rs-persona-play-state)}\n@media (forced-colors: active){.rs-persona-orb-surface{background-image:none;background-color:CanvasText;animation-name:none}}\n@media (prefers-reduced-motion: reduce){.rs-persona-orb-surface{animation-name:none}}\n.rs-persona-rings{display:grid;place-items:center;width:82%;height:82%;transform:scale(var(--rs-persona-visual-scale, 0.88)) scaleY(var(--rs-persona-ring-stretch));transition-property:transform;transition-duration:600ms;transition-timing-function:ease-in-out}\n@media (prefers-reduced-motion: reduce){.rs-persona-rings{transition-duration:0ms}}\n@media (forced-colors: active){.rs-persona-rings{transition-duration:0ms}}\n.rs-persona-ring{display:block;grid-area:1 / 1;box-sizing:border-box;border-width:1px;border-style:solid;border-color:currentColor;border-radius:50%;animation-name:rs-anim-resonate;animation-duration:3.6s;animation-timing-function:ease-in-out;animation-iteration-count:infinite;animation-play-state:var(--rs-persona-play-state)}\n@media (forced-colors: active){.rs-persona-ring{border-color:CanvasText;animation-name:none}}\n@media (prefers-reduced-motion: reduce){.rs-persona-ring{animation-name:none}}\n",
      "type": "registry:file",
      "target": "styles/vlak/persona.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "ai",
      "classes": [
        "rs-persona",
        "rs-persona-visual",
        "rs-persona-bar",
        "rs-persona-running",
        "rs-persona-listening",
        "rs-persona-thinking",
        "rs-persona-speaking",
        "rs-persona-asleep",
        "rs-persona-orb",
        "rs-persona-orb-surface",
        "rs-persona-rings",
        "rs-persona-ring"
      ],
      "snippet": "<div class=\"rs-persona rs-persona-thinking\" role=\"img\" aria-label=\"Assistant: thinking\" data-state=\"thinking\" data-variant=\"orb\" style=\"width:48px;height:48px\"><span class=\"rs-persona-orb\" aria-hidden=\"true\"><span class=\"rs-persona-orb-surface\"></span></span></div>",
      "cssOnly": false,
      "registryDependencies": [
        "vlak-lib"
      ],
      "aliases": [
        "AI Elements Persona",
        "AI avatar",
        "Assistant visual",
        "Voice state",
        "Orb"
      ],
      "example": "import { Persona, type PersonaState, type PersonaVariant, type PersonaVisualContext } from \"@noorddev/vlak-react\";\nimport type { ReactNode } from \"react\";\n\nexport function AssistantPersona({ state = \"idle\", variant = \"orb\", intensity, paused, renderVisual }: {\n  state?: PersonaState;\n  variant?: PersonaVariant;\n  intensity?: number;\n  paused?: boolean;\n  renderVisual?: (context: PersonaVisualContext) => ReactNode;\n}) {\n  return <Persona state={state} variant={variant} intensity={intensity} paused={paused}\n    size={48} label=\"Assistant\" renderVisual={renderVisual} />;\n}",
      "usage": {
        "use": [
          "Drive state from actual application recording, generation and playback events.",
          "Choose waveform, orb, or rings. Waveform remains the default. All five states work with each native visual without an external graphics runtime.",
          "Supply intensity from zero to one for measured input or output energy; it is clamped, and asleep always settles to zero. Otherwise each state provides a representative intensity.",
          "Motion pauses when offscreen, the document is hidden, paused is true, or state is asleep. Reduced motion and forced colors keep a static visual.",
          "onMotionChange reports the effective motion policy initially and when it changes. It does not report asset loading or custom engine playback events.",
          "Supply children for static custom content, or renderVisual for state, normalized size and intensity, and animated. A custom graphics renderer should honor animated and release its own resources.",
          "CSS-only markup shows a static representative visual; the React component manages visibility and user preference changes.",
          "Omit label for a decorative visual beside an already named response. Add label to expose the visual and state as an image."
        ],
        "avoid": [
          "Mounting a separate graphics runtime for every historical message.",
          "Using animation alone to communicate an error or progress that needs readable text."
        ]
      },
      "keyboard": [],
      "a11y": [
        "The default is decorative. A supplied label exposes a named image including the current conversational state.",
        "Animation respects reduced motion, forced colors, viewport visibility and document visibility.",
        "The component forwards native div attributes, class/style and ref; it does not request audio permissions or play speech."
      ]
    }
  }
}
