{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fx-shine-button",
  "type": "registry:component",
  "title": "Shine button",
  "description": "An action a soft sheen crosses under the pointer",
  "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-components.json",
    "https://gpuixui.sudokw.com/gpuix-motion.json"
  ],
  "files": [
    {
      "path": "packages/fx/src/shine-button.tsx",
      "target": "@ui/gpuixui/fx/shine-button.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport type { MotionLevel } from \"../motion/timing.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { useEffect } from \"react\";\nimport {\n  Button,\n  actionOnColor,\n  type ButtonProps,\n  measuredBox,\n  send,\n  useLocalState,\n  useLocalStore,\n  transitionWindowMs,\n  useMotionPreference,\n  useSurfaceTiming,\n  alpha,\n  useTheme,\n} from \"../components/authoring.js\";\nimport {\n  sheenFrames,\n  sheenGradient,\n  sheenHalf,\n  sheenOpacity,\n  type SheenHalf,\n} from \"./shine-button/logic.js\";\nimport { makeSheenStore } from \"./shine-button/store.js\";\n\nconst halves: ReadonlyArray<SheenHalf> = [\"lead\", \"trail\"];\n\nexport interface ShineButtonProps extends ButtonProps {\n  readonly level?: MotionLevel;\n\n  readonly sheenColor?: string;\n}\n\nexport const ShineButton = ({\n  variant = \"secondary\",\n  size = \"md\",\n  disabled = false,\n  level = \"notice\",\n  sheenColor,\n  onMouseEnter,\n  onLayout,\n  style,\n  children,\n  ...rest\n}: ShineButtonProps) => {\n  const theme = useTheme();\n  const preference = useMotionPreference();\n  const timing = useSurfaceTiming(level).enter;\n  const store = useLocalStore(makeSheenStore);\n  const { box, sweeping, run } = useLocalState(store.local);\n  const ink = sheenColor ?? actionOnColor(theme, variant);\n  const windowMs = transitionWindowMs(timing);\n  const inert = disabled || preference === \"reduced\";\n\n  useEffect(() => {\n    if (inert) send(store.clear);\n  }, [inert, store]);\n\n  useEffect(() => {\n    send(store.retime(windowMs));\n  }, [store, windowMs]);\n\n  const measure = (event: EventPayload) => {\n    send(store.measure(measuredBox(event)));\n    onLayout?.(event);\n  };\n\n  const enter = (event: EventPayload) => {\n    onMouseEnter?.(event);\n    send(store.enter(inert, windowMs));\n  };\n\n  const sheen = Option.match(box, {\n    onNone: () => null,\n    onSome: (measured) => {\n      const half = sheenHalf(measured.width);\n      return Arr.map(halves, (side) => (\n        <div\n          key={`${run}-${side}`}\n          testId=\"shine\"\n          motion={{\n            ...sheenFrames(half, measured.width, side),\n            transition: timing,\n          }}\n          style={{\n            position: \"absolute\",\n            top: 0,\n            width: half,\n            height: measured.height,\n            background: sheenGradient(\n              alpha(ink, sheenOpacity),\n              alpha(ink, 0),\n              side,\n            ),\n            pointerEvents: \"none\",\n          }}\n        />\n      ));\n    },\n  });\n\n  return (\n    <Button\n      {...rest}\n      variant={variant}\n      size={size}\n      disabled={disabled}\n      onMouseEnter={enter}\n      onLayout={measure}\n      style={mergeStyles({ position: \"relative\", overflow: \"hidden\" }, style)}\n    >\n      {children}\n      {sweeping ? sheen : null}\n    </Button>\n  );\n};\n\nexport const shineButtonMeta = {\n  name: \"Shine button\",\n  tier: 5,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the sheen travels on inset left, not a transform: the runtime scales nothing (upstream issue #102)\",\n    \"the band is two two-stop gradients, because 0.11.0's LinearGradientBackground takes exactly two stops\",\n    \"a reduced-motion reader, a keyboard reader and a disabled action get no sheen — including a band already crossing when the action is disabled; the hover fill and the focus ring are the remaining feedback\",\n    \"one sweep per hover entry; a repeating sheen needs the caller to re-enter or its own timer\",\n  ],\n  docs: \"docs/shine-button.mdx\",\n  example: \"examples/shine-button.example.tsx\",\n  testFile: \"src/behaviour/shine-button.test.tsx\",\n} as const;\n"
    },
    {
      "path": "packages/fx/src/shine-button/logic.ts",
      "target": "@ui/gpuixui/fx/shine-button/logic.ts",
      "type": "registry:lib",
      "content": "import type { LinearGradientBackground, MotionStyle } from \"@gpuix/react\";\n\nexport type SheenHalf = \"lead\" | \"trail\";\n\nexport const sheenOpacity = 0.2;\n\nexport interface SheenFrames {\n  readonly initial: MotionStyle;\n  readonly animate: MotionStyle;\n}\n\nexport const sheenHalf = (controlWidth: number): number =>\n  Math.max(16, Math.round(controlWidth / 4));\n\nexport const sheenFrames = (\n  half: number,\n  controlWidth: number,\n  side: SheenHalf,\n): SheenFrames => {\n  const offset = side === \"lead\" ? 0 : half;\n  return {\n    initial: { left: -2 * half + offset },\n    animate: { left: controlWidth + offset },\n  };\n};\n\nexport const sheenGradient = (\n  lit: string,\n  clear: string,\n  side: SheenHalf,\n): LinearGradientBackground => ({\n  type: \"linear-gradient\",\n  angle: 90,\n  stops:\n    side === \"lead\"\n      ? [\n          { color: clear, position: 0 },\n          { color: lit, position: 1 },\n        ]\n      : [\n          { color: lit, position: 0 },\n          { color: clear, position: 1 },\n        ],\n});\n"
    },
    {
      "path": "packages/fx/src/shine-button/store.ts",
      "target": "@ui/gpuixui/fx/shine-button/store.ts",
      "type": "registry:lib",
      "content": "import { Effect } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport * as SubscriptionRef from \"effect/SubscriptionRef\";\nimport {\n  type MeasuredBox,\n  makeLocalState,\n  type LocalState,\n} from \"../../components/authoring.js\";\n\nexport interface SheenState {\n  readonly box: Option.Option<MeasuredBox>;\n\n  readonly sweeping: boolean;\n\n  readonly run: number;\n}\n\nexport interface SheenStore {\n  readonly local: LocalState<SheenState>;\n\n  readonly measure: (box: Option.Option<MeasuredBox>) => Effect.Effect<void>;\n\n  readonly enter: (inert: boolean, windowMs: number) => Effect.Effect<void>;\n\n  readonly clear: Effect.Effect<void>;\n\n  readonly retime: (windowMs: number) => Effect.Effect<void>;\n}\n\nexport const makeSheenStore: Effect.Effect<SheenStore> = Effect.gen(\n  function* () {\n    const local = yield* makeLocalState<SheenState>({\n      box: Option.none(),\n      sweeping: false,\n      run: 0,\n    });\n\n    const end = Effect.gen(function* () {\n      const current = yield* SubscriptionRef.get(local.state);\n      if (!current.sweeping) return;\n      yield* local.write({ ...current, sweeping: false });\n    });\n\n    const measure = Effect.fnUntraced(function* (\n      box: Option.Option<MeasuredBox>,\n    ) {\n      const current = yield* SubscriptionRef.get(local.state);\n      yield* local.write({ ...current, box });\n    });\n\n    const enter = Effect.fnUntraced(function* (\n      inert: boolean,\n      windowMs: number,\n    ) {\n      if (inert) return;\n      const current = yield* SubscriptionRef.get(local.state);\n      yield* local.write({\n        ...current,\n        sweeping: true,\n        run: current.run + 1,\n      });\n      yield* local.after(windowMs, end);\n    });\n\n    const clear = Effect.gen(function* () {\n      yield* local.cancel;\n      yield* end;\n    });\n\n    const retime = Effect.fnUntraced(function* (windowMs: number) {\n      const current = yield* SubscriptionRef.get(local.state);\n      if (!current.sweeping) return;\n      yield* local.after(windowMs, end);\n    });\n\n    return { local, measure, enter, clear, retime };\n  },\n);\n"
    }
  ],
  "docs": "[Shine button](https://gpuixui.sudokw.com/effects/shine-button) — 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": "/effects/shine-button",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}