{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-gradient-text",
  "type": "registry:component",
  "title": "Gradient text",
  "description": "A phrase painted as a per-glyph colour ramp",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-motion.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/components/src/gradient-text.tsx",
      "target": "@ui/gpuixui/components/gradient-text.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport type { MotionLevel } from \"../motion/timing.js\";\nimport { cn } from \"../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport { fadeOnly, useSurfaceTiming } from \"./internal/presence.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { textOf } from \"./internal/text.js\";\nimport { textStyle, useTheme, type TextRole } from \"./internal/theme.js\";\nimport {\n  glyphCount,\n  gradientLines,\n  type GradientLine,\n} from \"./gradient-text/logic.js\";\n\nexport interface GradientTextState {\n  readonly text: string;\n\n  readonly glyphs: number;\n  readonly reducedMotion: boolean;\n}\n\nexport interface GradientTextProps extends Omit<Props, \"children\" | \"style\"> {\n  readonly text: string;\n\n  readonly from?: string;\n\n  readonly to?: string;\n\n  readonly role?: TextRole;\n\n  readonly level?: MotionLevel;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<GradientTextState>;\n}\n\nconst Line = ({\n  line,\n  metrics,\n  gap,\n}: {\n  readonly line: GradientLine;\n  readonly metrics: ReturnType<typeof textStyle>;\n  readonly gap: number;\n}) => (\n  <div\n    testId={`gradient-line-${line.key}`}\n    style={{\n      display: \"flex\",\n      flexDirection: \"row\",\n      flexWrap: \"wrap\",\n      alignItems: \"baseline\",\n      columnGap: gap,\n    }}\n  >\n    {line.words.map((word) => (\n      <div\n        key={word.key}\n        testId={`gradient-word-${word.key}`}\n        style={{\n          display: \"flex\",\n          flexDirection: \"row\",\n          alignItems: \"baseline\",\n        }}\n      >\n        {word.glyphs.map((glyph) => (\n          <text\n            key={glyph.key}\n            {...Option.match(glyph.label, {\n              onNone: () => ({ role: \"presentation\" as const }),\n              onSome: (label) => ({ \"aria-label\": label }),\n            })}\n            style={{ ...metrics, color: glyph.color }}\n          >\n            {glyph.text}\n          </text>\n        ))}\n      </div>\n    ))}\n  </div>\n);\n\nexport const GradientText = ({\n  text,\n  from,\n  to,\n  role = \"display\",\n  level = \"slow\",\n  className,\n  style,\n  ...rest\n}: GradientTextProps) => {\n  const theme = useTheme();\n  const timing = useSurfaceTiming(level);\n  const start = from ?? theme.colors.foreground;\n  const end = to ?? theme.colors.primary;\n\n  const phrase = textOf(text);\n  const lines = gradientLines(phrase, start, end);\n  const metrics = textStyle(theme, role);\n  const state: GradientTextState = {\n    text: phrase,\n    glyphs: glyphCount(lines),\n    reducedMotion: timing.enter.duration === 0,\n  };\n\n  return (\n    <div\n      {...rest}\n      motion={{ ...fadeOnly, transition: timing.enter }}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(className), theme), {\n          display: \"flex\",\n          flexDirection: \"column\",\n        }),\n        resolveStyle(style, state),\n      )}\n    >\n      {lines.map((line) => (\n        <Line\n          key={line.key}\n          line={line}\n          metrics={metrics}\n          gap={Math.round(metrics.fontSize * 0.3)}\n        />\n      ))}\n    </div>\n  );\n};\n\nexport const gradientTextMeta = {\n  name: \"Gradient text\",\n  tier: 5,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"no text fill in 0.11.0: the ramp is sampled per glyph into each code point's own colour, so the blend is discrete at glyph resolution rather than continuous\",\n    \"both ends must be hex colours; an end that is not one paints the whole run in the start colour\",\n    \"the ramp restarts on each explicit line rather than continuing across the block\",\n    \"a word is one row of glyph hosts, so a long paragraph is many nodes; wrapping happens between words\",\n    \"the ramp itself does not travel: recolouring would need a per-frame React loop, and there is no transform to move a text layer instead\",\n    \"the accessible reading is one label per word, because 0.11.0 has no aria-hidden and every glyph is its own host\",\n  ],\n  docs: \"docs/gradient-text.mdx\",\n  example: \"examples/gradient-text.example.tsx\",\n  testFile: \"src/behaviour/gradient-text.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/gradient-text/logic.ts",
      "target": "@ui/gpuixui/components/gradient-text/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Str from \"effect/String\";\n\nexport interface HexColor {\n  readonly red: number;\n  readonly green: number;\n  readonly blue: number;\n  readonly alpha: number;\n}\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;\n\nconst byteAt = (digits: string, index: number, width: number): number => {\n  const start = index * width;\n  const part = Str.slice(start, start + width)(digits);\n  return Number.parseInt(width === 1 ? `${part}${part}` : part, 16);\n};\n\nexport const parseHexColor = (value: string): Option.Option<HexColor> =>\n  Option.flatMap(Option.fromNullishOr(HEX.exec(value)), (match) => {\n    const digits = Str.toLowerCase(match[1] ?? \"\");\n    const width = digits.length <= 4 ? 1 : 2;\n    return Option.some({\n      red: byteAt(digits, 0, width),\n      green: byteAt(digits, 1, width),\n      blue: byteAt(digits, 2, width),\n      alpha:\n        digits.length === 4 || digits.length === 8\n          ? byteAt(digits, 3, width)\n          : 255,\n    });\n  });\n\nconst hexByte = (value: number): string =>\n  Str.padStart(2, \"0\")(Math.round(value).toString(16));\n\nconst writeHex = (color: HexColor): string =>\n  `#${hexByte(color.red)}${hexByte(color.green)}${hexByte(color.blue)}${\n    color.alpha === 255 ? \"\" : hexByte(color.alpha)\n  }`;\n\nconst clamped = (t: number): number =>\n  Number.isFinite(t) ? Math.min(Math.max(t, 0), 1) : 0;\n\nexport const gradientColor = (start: string, end: string, t: number): string =>\n  Option.match(Option.all([parseHexColor(start), parseHexColor(end)]), {\n    onNone: () => start,\n    onSome: ([from, to]) => {\n      const at = clamped(t);\n      return writeHex({\n        red: from.red + (to.red - from.red) * at,\n        green: from.green + (to.green - from.green) * at,\n        blue: from.blue + (to.blue - from.blue) * at,\n        alpha: from.alpha + (to.alpha - from.alpha) * at,\n      });\n    },\n  });\n\nexport interface GradientGlyph {\n  readonly key: string;\n  readonly text: string;\n  readonly color: string;\n\n  readonly position: number;\n\n  readonly label: Option.Option<string>;\n}\n\nexport interface GradientWord {\n  readonly key: string;\n  readonly label: string;\n  readonly glyphs: ReadonlyArray<GradientGlyph>;\n}\n\nexport interface GradientLine {\n  readonly key: string;\n  readonly words: ReadonlyArray<GradientWord>;\n}\n\nconst wordGlyphs = (\n  word: string,\n  line: number,\n  start: number,\n  total: number,\n  colorAt: (position: number) => string,\n): ReadonlyArray<GradientGlyph> =>\n  Arr.map(Arr.fromIterable(word), (text, local) => {\n    const index = start + local;\n    const position = total <= 1 ? 0 : index / (total - 1);\n    return {\n      key: `${line}:${index}:${text}`,\n      text,\n      color: colorAt(position),\n      position,\n      label: local === 0 ? Option.some(word) : Option.none(),\n    };\n  });\n\nexport const glyphLines = (\n  text: string,\n  colorAt: (position: number) => string,\n): ReadonlyArray<GradientLine> =>\n  Arr.filterMap(\n    Arr.map(Str.split(text, \"\\n\"), (line, index) => ({\n      index,\n      words: Arr.filter(Str.split(line, /\\s+/), Str.isNonEmpty),\n    })),\n    ({ index, words }) =>\n      Arr.match(words, {\n        onEmpty: () => Result.failVoid,\n        onNonEmpty: (present) => {\n          const total = Arr.reduce(\n            present,\n            0,\n            (count, word) => count + Arr.length(Arr.fromIterable(word)),\n          );\n          const placed = Arr.mapAccum(present, 0, (start, word) => [\n            start + Arr.length(Arr.fromIterable(word)),\n            {\n              key: `${index}:${start}:${word}`,\n              label: word,\n              glyphs: wordGlyphs(word, index, start, total, colorAt),\n            },\n          ]);\n          return Result.succeed({ key: String(index), words: placed[1] });\n        },\n      }),\n  );\n\nexport const gradientLines = (\n  text: string,\n  from: string,\n  to: string,\n): ReadonlyArray<GradientLine> =>\n  glyphLines(text, (position) => gradientColor(from, to, position));\n\nexport const glyphCount = (lines: ReadonlyArray<GradientLine>): number =>\n  Arr.reduce(\n    lines,\n    0,\n    (count, line) =>\n      count +\n      Arr.reduce(line.words, 0, (inLine, word) => inLine + word.glyphs.length),\n  );\n"
    },
    {
      "path": "packages/components/src/internal/presence.tsx",
      "target": "@ui/gpuixui/components/internal/presence.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { MotionProps } from \"@gpuix/react\";\nimport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceMotion as motionOfPresence,\n  presenceStyle,\n  transitionWindowMs,\n  type PresenceFrames,\n  type PresenceHitRule,\n  type PresenceSide,\n  type PresenceTiming,\n  type PresenceTransition,\n} from \"../../motion/presence.js\";\nimport {\n  MotionProvider,\n  useMotionPreference,\n  useSurfacePresence,\n  useSurfaceTiming,\n  type MotionProviderProps,\n  type SurfacePresence,\n} from \"../../motion/react/presence.js\";\nimport {\n  motionTiming,\n  type MotionLevel,\n  type MotionPreference,\n} from \"../../motion/timing.js\";\n\nexport { MotionProvider, useMotionPreference, type MotionProviderProps };\n\nexport type SurfaceFrames = PresenceFrames;\n\nexport type SurfaceTiming = PresenceTiming;\n\nexport type PanelSide = PresenceSide;\n\nexport {\n  delayedTiming,\n  exitWindowMs,\n  fadeFrom,\n  fadeFromAnchor,\n  fadeFromSide,\n  fadeOnly,\n  presenceStyle,\n  transitionWindowMs,\n};\nexport type { PresenceHitRule };\n\nexport { useSurfacePresence, useSurfaceTiming, type SurfacePresence };\n\nexport const moveTiming = (\n  level: MotionLevel,\n  preference: MotionPreference,\n): PresenceTransition => motionTiming(level, { phase: \"standard\", preference });\n\nexport const useMoveTiming = (level: MotionLevel): PresenceTransition =>\n  moveTiming(level, useMotionPreference());\n\nexport const presenceMotion = (\n  frames: SurfaceFrames,\n  presence: SurfacePresence,\n  timing: SurfaceTiming = presence.timing,\n): MotionProps => motionOfPresence(frames, presence.present, timing);\n\nexport const PresenceShield = ({ present }: { readonly present: boolean }) =>\n  present ? null : (\n    <div\n      testId=\"presence-shield\"\n      style={{ position: \"absolute\", top: 0, right: 0, bottom: 0, left: 0 }}\n    />\n  );\n\nexport const presenceTabStop = (\n  present: boolean,\n): { readonly tabIndex?: number } => (present ? {} : { tabIndex: -1 });\n"
    },
    {
      "path": "packages/components/src/internal/recipe.ts",
      "target": "@ui/gpuixui/components/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(512);\n"
    },
    {
      "path": "packages/components/src/internal/text.ts",
      "target": "@ui/gpuixui/components/internal/text.ts",
      "type": "registry:lib",
      "content": "import { pipe } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as Str from \"effect/String\";\n\nexport const textOf = (value: unknown): string =>\n  typeof value === \"string\" ? value : \"\";\n\nexport const nameOf = (value: unknown): string | undefined =>\n  pipe(\n    Option.some(textOf(value)),\n    Option.map(Str.trim),\n    Option.filter(Str.isNonEmpty),\n    Option.getOrUndefined,\n  );\n"
    },
    {
      "path": "packages/components/src/internal/theme.ts",
      "target": "@ui/gpuixui/components/internal/theme.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport * as Schema from \"effect/Schema\";\nimport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  brandTextOn,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  pointerWash,\n  selectionOn,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n  type BoxShadow,\n  type ElevationLevel,\n  type FindHighlightSource,\n  type NativeTextFill,\n  type PlatformName,\n  type PointerWashSurface,\n  type StatusTone,\n  type SyntaxColors,\n  type SyntaxRole,\n  type TextRole,\n  type Theme,\n  type ThemeColors,\n} from \"../../tokens/index.js\";\n\nexport {\n  alpha,\n  avatarPixels,\n  avatarSize,\n  controlFocusRing,\n  controlHeight,\n  controlPaddingX,\n  currentPlatform,\n  darkTheme,\n  disabledAction,\n  disabledOpacity,\n  elevationShadow,\n  filledHover,\n  filledPressed,\n  findHighlight,\n  focusRing,\n  keyboardFocusRing,\n  lightTheme,\n  nativeTextTheme,\n  statusTone,\n  syntaxColors,\n  targetSize,\n  textStyle,\n};\nexport type {\n  BoxShadow,\n  ElevationLevel,\n  FindHighlightSource,\n  NativeTextFill,\n  PlatformName,\n  StatusTone,\n  SyntaxColors,\n  SyntaxRole,\n  TextRole,\n  Theme,\n};\nexport { ThemeProvider, useTheme } from \"../../tokens/react.js\";\n\nexport const keyboardRing = (theme: Theme, invalid = false): StyleDesc =>\n  controlFocusRing(theme, { invalid }) ?? {};\n\nexport interface ControlRingState {\n  readonly disabled?: boolean;\n\n  readonly invalid?: boolean;\n}\n\nexport const controlRing = (\n  theme: Theme,\n  state: ControlRingState = {},\n): StyleDesc => controlFocusRing(theme, state) ?? {};\n\nexport const pressWash = (theme: Theme, replaced: PointerWashSurface): string =>\n  pointerWash(theme, \"active\", replaced) ?? theme.colors.active;\n\nexport const selectionWash = (theme: Theme): StyleDesc => ({\n  selectionColor: theme.colors.selection,\n});\n\nexport const keepContent: StyleDesc = { keyboardFocusDim: false };\n\nexport const disabledFade = (\n  disabled: boolean,\n): { readonly opacity: number } | undefined =>\n  disabled ? { opacity: disabledOpacity } : undefined;\n\nconst Tones = Schema.Literals([\"info\", \"success\", \"warning\", \"danger\"]);\n\nexport type Tone = typeof Tones.Type;\n\nexport const toneOf = (value: unknown): Tone =>\n  Schema.is(Tones)(value) ? value : \"info\";\n\nexport interface ToneStyle {\n  readonly fg: string;\n  readonly bg: string;\n  readonly border: string;\n}\n\nconst TOKEN_TONE: Readonly<Record<Tone, StatusTone>> = {\n  info: \"info\",\n  success: \"success\",\n  warning: \"warning\",\n  danger: \"destructive\",\n};\n\nexport const tone = (theme: Theme, value: Tone): ToneStyle => {\n  const { background, border, foreground } = statusTone(\n    theme,\n    TOKEN_TONE[value],\n  );\n  return { bg: background, border, fg: foreground };\n};\n\nconst softRoles: Readonly<Record<Tone, keyof ThemeColors>> = {\n  info: \"infoSoft\",\n  success: \"successSoft\",\n  warning: \"warningSoft\",\n  danger: \"destructiveSoft\",\n};\n\nexport const messageLink = (theme: Theme, value: Tone): string =>\n  brandTextOn(theme, softRoles[value]);\n\nexport const messageSelection = (theme: Theme, value: Tone): string =>\n  selectionOn(theme, softRoles[value]);\n\nexport const toneSurface = (theme: Theme, value: Tone): StyleDesc => {\n  const { bg, border, fg } = tone(theme, value);\n  return {\n    backgroundColor: bg,\n    color: fg,\n    borderWidth: theme.borderWidth.thin,\n    borderColor: border,\n  };\n};\n"
    }
  ],
  "docs": "[Gradient text](https://gpuixui.sudokw.com/components/gradient-text) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/components/gradient-text",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}