{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-gradient",
  "type": "registry:component",
  "title": "Gradient",
  "description": "A two-stop gradient surface that breathes between ramps",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "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.tsx",
      "target": "@ui/gpuixui/components/gradient.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { MotionProps, Props, StyleDesc } 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 { useState } from \"react\";\nimport {\n  fadeOnly,\n  useMotionPreference,\n  useSurfaceTiming,\n} from \"./internal/presence.js\";\nimport { recipeStyle } from \"./internal/recipe.js\";\nimport { alpha, useTheme } from \"./internal/theme.js\";\nimport {\n  angleOf,\n  breathesAtAll,\n  legOf,\n  radiusOf,\n  rampOf,\n  shiftOf,\n  shiftedRampOf,\n} from \"./gradient/logic.js\";\n\nexport interface GradientState {\n  readonly angle: number;\n\n  readonly shift: number;\n\n  readonly animated: boolean;\n  readonly reducedMotion: boolean;\n}\n\nexport interface GradientProps extends Omit<Props, \"style\"> {\n  readonly from?: string;\n\n  readonly to?: string;\n\n  readonly angle?: number;\n\n  readonly shift?: number;\n\n  readonly durationMs?: number;\n\n  readonly radius?: number;\n\n  readonly level?: MotionLevel;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<GradientState>;\n}\n\ninterface BreathProps {\n  readonly ramp: ReturnType<typeof rampOf>;\n  readonly legMs: number;\n  readonly radius: number;\n}\n\nconst Breath = ({ ramp, legMs, radius }: BreathProps) => {\n  const [lit, setLit] = useState(false);\n  const motion: MotionProps = {\n    initial: { opacity: 0 },\n    animate: { opacity: lit ? 0 : 1 },\n    transition: { duration: legMs / 1000, ease: \"easeInOut\" },\n  };\n\n  return (\n    <div\n      testId=\"gradient-shift\"\n      motion={motion}\n      onMotionComplete={() => setLit((current) => !current)}\n      style={{\n        position: \"absolute\",\n        top: 0,\n        right: 0,\n        bottom: 0,\n        left: 0,\n        background: ramp,\n        borderRadius: radius,\n        opacity: 0,\n\n        pointerEvents: \"none\",\n      }}\n    />\n  );\n};\n\nexport const Gradient = ({\n  from,\n  to,\n  angle,\n  shift,\n  durationMs,\n  radius,\n  level = \"slow\",\n  className,\n  style,\n  children,\n  ...rest\n}: GradientProps) => {\n  const theme = useTheme();\n  const preference = useMotionPreference();\n  const timing = useSurfaceTiming(level);\n  const baseAngle = angleOf(Option.fromNullishOr(angle));\n  const turn = shiftOf(Option.fromNullishOr(shift));\n  const legMs = legOf(Option.fromNullishOr(durationMs));\n  const rounded = radiusOf(Option.fromNullishOr(radius));\n  const still = preference === \"reduced\";\n  const start = from ?? theme.colors.primary;\n  const end = to ?? alpha(theme.colors.primary, 0);\n  const breathing = !still && breathesAtAll(turn);\n  const shape: StyleDesc = rounded > 0 ? { borderRadius: rounded } : {};\n\n  const state: GradientState = {\n    angle: baseAngle,\n    shift: turn,\n    animated: breathing,\n    reducedMotion: still,\n  };\n\n  return (\n    <div\n      {...rest}\n      motion={{ ...fadeOnly, transition: timing.enter }}\n      style={mergeStyles(\n        mergeStyles(mergeStyles(recipeStyle(cn(className), theme), shape), {\n          position: \"relative\",\n          background: rampOf(start, end, baseAngle),\n        }),\n        resolveStyle(style, state),\n      )}\n    >\n      {breathing ? (\n        <Breath\n          ramp={shiftedRampOf(start, end, baseAngle, turn)}\n          legMs={legMs}\n          radius={rounded}\n        />\n      ) : undefined}\n      {children}\n    </div>\n  );\n};\n\nexport const gradientMeta = {\n  name: \"Gradient\",\n  tier: 5,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the runtime paints exactly two gradient stops, so a three-colour or multi-stop ramp is not expressible; the movement is two ramps of the same pair crossfaded, not a gradient whose stops travel\",\n    \"there is no transform (upstream issue #102), so the second ramp is re-drawn at another angle rather than rotated or scaled\",\n    \"the layers are rectangles: a rounded field passes `radius`, which is applied to the field and its layer, because the runtime does not document a rounded overflow clip\",\n    \"the reduced preference paints the base ramp alone, with no second layer and no motion target\",\n    \"the field is decoration in the sense that it owns no text, but it is a surface: content passed as children is painted above it and keeps its presses\",\n    \"a hidden field keeps breathing (the runtime exposes no visibility query), so unmount it or pass `shift={0}` when it is off screen\",\n  ],\n  docs: \"docs/gradient.mdx\",\n  example: \"examples/gradient.example.tsx\",\n  testFile: \"src/behaviour/gradient.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/components/src/gradient/logic.ts",
      "target": "@ui/gpuixui/components/gradient/logic.ts",
      "type": "registry:lib",
      "content": "import type { LinearGradientBackground } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\nimport { durationOf, paintable } from \"../internal/scatter.js\";\n\nexport const gradientBaseAngle = 135;\n\nexport const gradientBaseShift = 90;\n\nexport const gradientLegMs = 6_000;\n\nexport const gradientLegFloor = 400;\n\nexport const gradientLegCap = 60_000;\n\nexport const oneTurn = (degrees: number): number =>\n  ((degrees % 360) + 360) % 360;\n\nexport const angleOf = (angle: Option.Option<number>): number =>\n  Option.getOrElse(\n    Option.map(\n      Option.filter(angle, (value) => Number.isFinite(value)),\n      oneTurn,\n    ),\n    () => gradientBaseAngle,\n  );\n\nexport const shiftOf = (shift: Option.Option<number>): number =>\n  Option.getOrElse(\n    Option.map(\n      Option.filter(shift, (value) => Number.isFinite(value)),\n      oneTurn,\n    ),\n    () => gradientBaseShift,\n  );\n\nexport const breathesAtAll = (shift: number): boolean =>\n  Number.isFinite(shift) && oneTurn(shift) !== 0;\n\nexport const legOf = (leg: Option.Option<number>): number =>\n  durationOf(leg, gradientLegMs, gradientLegFloor, gradientLegCap);\n\nexport const radiusOf = (radius: Option.Option<number>): number =>\n  Option.getOrElse(\n    Option.filter(radius, (value) => paintable(value) && value <= 10_000),\n    () => 0,\n  );\n\nexport const rampOf = (\n  from: string,\n  to: string,\n  angle: number,\n): LinearGradientBackground => ({\n  type: \"linear-gradient\",\n  angle: oneTurn(angle),\n  stops: [\n    { color: from, position: 0 },\n    { color: to, position: 1 },\n  ],\n});\n\nexport const shiftedRampOf = (\n  from: string,\n  to: string,\n  angle: number,\n  shift: number,\n): LinearGradientBackground => rampOf(from, to, angle + shift);\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/scatter.ts",
      "target": "@ui/gpuixui/components/internal/scatter.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\n\nexport const seededUnit = (\n  seed: number,\n  index: number,\n  slot: number,\n): number => {\n  let value =\n    Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b) ^\n    Math.imul((index + 1) * 8 + slot, 0xc2b2ae35);\n  value ^= value >>> 15;\n  value = Math.imul(value, 0x2c1b3c6d);\n  value ^= value >>> 13;\n  return (value >>> 0) / 0x100000000;\n};\n\nexport const tenth = (value: number): number => Math.round(value * 10) / 10;\n\nexport const paintable = (value: number): boolean =>\n  Number.isFinite(value) && value > 0;\n\nexport const seedOf = (seed: Option.Option<number>): number =>\n  Option.getOrElse(\n    Option.filter(seed, (value) => Number.isFinite(value)),\n    () => 1,\n  );\n\nexport const countOf = (\n  count: Option.Option<number>,\n  fallback: number,\n  cap: number,\n): number =>\n  Option.getOrElse(\n    Option.map(\n      Option.filter(count, (value) => Number.isFinite(value) && value >= 0),\n      (value) => Math.min(Math.round(value), cap),\n    ),\n    () => fallback,\n  );\n\nexport const sizeRangeOf = (\n  min: Option.Option<number>,\n  max: Option.Option<number>,\n  fallbackMin: number,\n  fallbackMax: number,\n): { readonly min: number; readonly max: number } => {\n  const floor = sizeOf(min, fallbackMin);\n  const ceiling = sizeOf(max, fallbackMax);\n  return floor <= ceiling\n    ? { min: floor, max: ceiling }\n    : { min: ceiling, max: floor };\n};\n\nconst sizeOf = (size: Option.Option<number>, fallback: number): number =>\n  Option.getOrElse(\n    Option.map(\n      Option.filter(size, (value) => Number.isFinite(value) && value >= 1),\n      Math.round,\n    ),\n    () => fallback,\n  );\n\nexport const durationOf = (\n  duration: Option.Option<number>,\n  fallback: number,\n  floor: number,\n  cap: number,\n): number =>\n  Option.getOrElse(\n    Option.map(\n      Option.filter(\n        duration,\n        (value) => Number.isFinite(value) && value >= floor,\n      ),\n      (value) => Math.min(Math.round(value), cap),\n    ),\n    () => fallback,\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](https://gpuixui.sudokw.com/components/gradient) — 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",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}