{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glint-card",
  "type": "registry:item",
  "title": "Glint Card",
  "description": "A card with a specular glint that slides with the finger or the tilt of the device.",
  "categories": [
    "card"
  ],
  "dependencies": [
    "@shopify/react-native-skia",
    "react-native-gesture-handler",
    "react-native-reanimated",
    "react-native-worklets"
  ],
  "registryDependencies": [
    "https://kinetik-ui.dev/r/kinetik-tokens.json",
    "https://kinetik-ui.dev/r/kinetik-motion.json",
    "https://kinetik-ui.dev/r/kinetik-tilt.json"
  ],
  "files": [
    {
      "path": "src/components/kinetik/glint-card.tsx",
      "type": "registry:file",
      "target": "~/components/kinetik/glint-card.tsx",
      "content": "import {\n  Canvas,\n  Group,\n  LinearGradient,\n  RadialGradient,\n  RoundedRect,\n  rrect,\n  rect,\n  vec,\n  type SkSize,\n} from '@shopify/react-native-skia';\nimport type { ReactNode } from 'react';\nimport { StyleSheet, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';\nimport { Gesture, GestureDetector } from 'react-native-gesture-handler';\nimport Animated, {\n  useAnimatedReaction,\n  useAnimatedStyle,\n  useDerivedValue,\n  useSharedValue,\n  withSpring,\n} from 'react-native-reanimated';\n\nimport { TiltSensor, useTilt } from '../../lib/kinetik/hooks/use-tilt';\nimport { useReduceMotion } from '../../lib/kinetik/hooks/use-reduce-motion';\nimport { spring } from '../../lib/kinetik/motion/motion';\nimport { useKinetikTheme } from '../../lib/kinetik/tokens/theme';\nimport { withAlpha } from '../../lib/kinetik/tokens/tokens';\n\nexport type GlintSource = 'touch' | 'tilt' | 'both';\n\nexport type GlintCardProps = Omit<ViewProps, 'style' | 'children'> & {\n  children?: ReactNode;\n  /** What moves the light. Defaults to `both`: finger while touching, device tilt otherwise. */\n  source?: GlintSource;\n  /** Strength of the glint, 0–1. Defaults to 0.7. */\n  intensity?: number;\n  /** Colour of the reflected light. Defaults to the theme highlight. */\n  glintColor?: string;\n  /** Maximum card rotation in degrees as the light moves. 0 keeps the card flat. Defaults to 4. */\n  tiltDegrees?: number;\n  /** Corner radius. Defaults to the theme `xl` radius. */\n  radius?: number;\n  /**\n   * Static reflection with no sensor or gesture work. Use on low-power devices\n   * or in long lists. Reduced-motion users always get this.\n   */\n  reduceEffects?: boolean;\n  /** Pause sensor and gesture work, e.g. while the card is off-screen. Defaults to true. */\n  active?: boolean;\n  style?: StyleProp<ViewStyle>;\n  /** Style of the inner content container (padding, layout). */\n  contentStyle?: StyleProp<ViewStyle>;\n};\n\nconst REST = { x: -0.35, y: -0.45 };\n\n/**\n * A surface with a specular glint that slides with the finger or the tilt of\n * the device, plus a rim light on the edge facing the light.\n */\nexport function GlintCard({\n  children,\n  source = 'both',\n  intensity = 0.7,\n  glintColor,\n  tiltDegrees = 4,\n  radius,\n  reduceEffects = false,\n  active = true,\n  style,\n  contentStyle,\n  ...viewProps\n}: GlintCardProps) {\n  const theme = useKinetikTheme();\n  const reduced = useReduceMotion();\n  const still = reduced || reduceEffects;\n  const r = radius ?? theme.radius.xl;\n  const light = glintColor ?? theme.colors.highlight;\n  const k = Math.max(0, Math.min(1, intensity));\n  // A white glint cannot brighten a white card. On light surfaces the light is\n  // shown by shading everything it does not hit, as on glossy paper.\n  const paper = theme.scheme === 'light' && !glintColor;\n  const shade = theme.colors.text;\n\n  const tilt = useTilt();\n  const touching = useSharedValue(false);\n  const touch = useSharedValue({ x: 0, y: 0 });\n  const lx = useSharedValue(REST.x);\n  const ly = useSharedValue(REST.y);\n  const size = useSharedValue<SkSize>({ width: 0, height: 0 });\n\n  const followTouch = source !== 'tilt';\n  const followTilt = source !== 'touch';\n  const live = active && !still;\n\n  // Pick the light target on the UI thread and ease toward it.\n  useAnimatedReaction(\n    () => {\n      if (!live) return REST;\n      if (touching.get() && followTouch) return touch.get();\n      if (followTilt) return { x: REST.x + tilt.x.get() * 0.9, y: REST.y + tilt.y.get() * 0.9 };\n      return REST;\n    },\n    (target, previous) => {\n      if (previous && target.x === previous.x && target.y === previous.y) return;\n      const fast = touching.get();\n      lx.set(withSpring(target.x, spring(fast ? 'snappy' : 'smooth')));\n      ly.set(withSpring(target.y, spring(fast ? 'snappy' : 'smooth')));\n    },\n    [live, followTouch, followTilt],\n  );\n\n  const gesture = Gesture.Manual()\n    .withTestId('glint-card-touch')\n    .enabled(live && followTouch)\n    .onTouchesDown((e) => {\n      const t = e.allTouches[0];\n      const { width, height } = size.get();\n      if (!t || width === 0 || height === 0) return;\n      touch.set({ x: (t.x / width) * 2 - 1, y: (t.y / height) * 2 - 1 });\n      touching.set(true);\n    })\n    .onTouchesMove((e) => {\n      const t = e.allTouches[0];\n      const { width, height } = size.get();\n      if (!t || width === 0 || height === 0) return;\n      touch.set({\n        x: Math.max(-1.2, Math.min(1.2, (t.x / width) * 2 - 1)),\n        y: Math.max(-1.2, Math.min(1.2, (t.y / height) * 2 - 1)),\n      });\n    })\n    .onTouchesUp(() => {\n      touching.set(false);\n    })\n    .onTouchesCancelled(() => {\n      touching.set(false);\n    });\n\n  const cardStyle = useAnimatedStyle(() => {\n    if (tiltDegrees === 0) return {};\n    return {\n      transform: [\n        { perspective: 900 },\n        { rotateX: `${(-(ly.get() - REST.y) * tiltDegrees).toFixed(3)}deg` },\n        { rotateY: `${((lx.get() - REST.x) * tiltDegrees).toFixed(3)}deg` },\n      ],\n    };\n  });\n\n  const clip = useDerivedValue(() => rrect(rect(0, 0, size.get().width, size.get().height), r, r));\n\n  // Soft hotspot where the light hits the surface.\n  const hotspotCenter = useDerivedValue(() => {\n    const { width, height } = size.get();\n    return vec(width * (0.5 + lx.get() * 0.55), height * (0.5 + ly.get() * 0.55));\n  });\n  const hotspotRadius = useDerivedValue(() => Math.max(size.get().width, size.get().height) * 0.75);\n\n  // A diagonal sheen band that sweeps across as the light moves.\n  const bandStart = useDerivedValue(() => vec(0, 0));\n  const bandEnd = useDerivedValue(() => vec(size.get().width, size.get().height));\n  const bandPositions = useDerivedValue(() => {\n    const t = Math.max(0.2, Math.min(0.8, 0.5 + lx.get() * 0.42 + ly.get() * 0.18));\n    return [0, t - 0.18, t - 0.05, t - 0.012, t, t + 0.012, t + 0.05, t + 0.18, 1];\n  });\n\n  // Rim light: brighter on the edge facing the light.\n  const rimStart = useDerivedValue(() => {\n    const { width, height } = size.get();\n    return vec(width * (0.5 + lx.get() * 0.5), height * (0.5 + ly.get() * 0.5));\n  });\n  const rimEnd = useDerivedValue(() => {\n    const { width, height } = size.get();\n    return vec(width * (0.5 - lx.get() * 0.5), height * (0.5 - ly.get() * 0.5));\n  });\n  const rimRect = useDerivedValue(() =>\n    rrect(rect(0.5, 0.5, size.get().width - 1, size.get().height - 1), r - 0.5, r - 0.5),\n  );\n\n  const clear = withAlpha(light, 0);\n  const bandColors = [\n    clear,\n    clear,\n    withAlpha(light, 0.1 * k),\n    withAlpha(light, 0.22 * k),\n    withAlpha(light, 0.4 * k),\n    withAlpha(light, 0.22 * k),\n    withAlpha(light, 0.1 * k),\n    clear,\n    clear,\n  ];\n  const unlit = withAlpha(shade, 0);\n  const dim = withAlpha(shade, 0.07 * k);\n  const shadeColors = [\n    dim,\n    dim,\n    withAlpha(shade, 0.03 * k),\n    withAlpha(shade, 0.01 * k),\n    unlit,\n    withAlpha(shade, 0.01 * k),\n    withAlpha(shade, 0.03 * k),\n    dim,\n    dim,\n  ];\n\n  return (\n    <GestureDetector gesture={gesture}>\n      <Animated.View\n        collapsable={false}\n        {...viewProps}\n        style={[\n          styles.card,\n          {\n            borderRadius: r,\n            backgroundColor: theme.colors.surface,\n            borderColor: theme.colors.border,\n            boxShadow: theme.shadow.md,\n          },\n          style,\n          cardStyle,\n        ]}\n      >\n        <View style={[styles.content, { padding: theme.space[5] }, contentStyle]}>{children}</View>\n        {/* The wrapper lets touches through: on the web the canvas itself ignores pointerEvents. */}\n        <View\n          pointerEvents=\"none\"\n          style={StyleSheet.absoluteFill}\n          accessibilityElementsHidden\n          importantForAccessibility=\"no-hide-descendants\"\n        >\n          <Canvas style={StyleSheet.absoluteFill} onSize={size}>\n            {paper ? (\n              <Group clip={clip} blendMode=\"multiply\">\n                <RoundedRect rect={clip}>\n                  <RadialGradient c={hotspotCenter} r={hotspotRadius} colors={[unlit, dim]} />\n                </RoundedRect>\n                <RoundedRect rect={clip}>\n                  <LinearGradient\n                    start={bandStart}\n                    end={bandEnd}\n                    colors={shadeColors}\n                    positions={bandPositions}\n                  />\n                </RoundedRect>\n              </Group>\n            ) : (\n              <Group clip={clip} blendMode=\"screen\">\n                <RoundedRect rect={clip}>\n                  <RadialGradient\n                    c={hotspotCenter}\n                    r={hotspotRadius}\n                    colors={[withAlpha(light, 0.16 * k), clear]}\n                  />\n                </RoundedRect>\n                <RoundedRect rect={clip}>\n                  <LinearGradient\n                    start={bandStart}\n                    end={bandEnd}\n                    colors={bandColors}\n                    positions={bandPositions}\n                  />\n                </RoundedRect>\n              </Group>\n            )}\n            <RoundedRect rect={rimRect} style=\"stroke\" strokeWidth={1}>\n              <LinearGradient\n                start={rimStart}\n                end={rimEnd}\n                colors={\n                  paper\n                    ? // The edge facing away from the light falls into shadow.\n                      [withAlpha(shade, 0.02 * k), withAlpha(shade, 0.22 * k)]\n                    : [withAlpha(light, 0.55 * k), withAlpha(light, 0.04 * k)]\n                }\n              />\n            </RoundedRect>\n          </Canvas>\n        </View>\n        {live && followTilt && <TiltSensor target={tilt} />}\n      </Animated.View>\n    </GestureDetector>\n  );\n}\n\nconst styles = StyleSheet.create({\n  card: {\n    borderWidth: StyleSheet.hairlineWidth,\n    overflow: 'hidden',\n  },\n  content: {\n    flexGrow: 1,\n  },\n});\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "kind": "component",
    "platforms": [
      "ios",
      "android",
      "web"
    ],
    "category": "card",
    "web": "live",
    "perf": {
      "budget": "Idle: no per-frame work. Moving: three gradients redrawn by Skia on the UI thread.",
      "measurements": [
        {
          "device": "Pixel 9 emulator, API 35",
          "kind": "emulator",
          "date": "2026-10-12",
          "build": "release",
          "frameMs": 32,
          "p90Ms": 38,
          "baselineMs": 20,
          "jankyPercent": 4.97,
          "atRest": "idle",
          "memoryMb": 164,
          "notes": "Tilt changes below the visible threshold are ignored, so a phone at rest stops redrawing (was 113 frames per 5 s before). Heaviest interaction cost in the set: three gradients and a 3D transform per frame."
        }
      ]
    },
    "a11y": {
      "reducedMotion": "Static reflection; tilt sensor and touch tracking are not started.",
      "screenReader": "Canvas is hidden from assistive tech; pass accessibilityLabel/Role to describe the card.",
      "touchTarget": "Not interactive by itself; any controls inside keep their own targets."
    },
    "requires": "React Native New Architecture (Reanimated 4)",
    "guide": "## Usage\n\n```tsx\nimport { GlintCard } from '@/components/kinetik/glint-card';\n\nexport function Pass() {\n  return (\n    <GlintCard accessibilityLabel=\"Night pass, gate 4\">\n      <Text>Night pass</Text>\n    </GlintCard>\n  );\n}\n```\n\nThe light follows the finger while it touches the card and the device tilt the rest of the time. The touch tracking never claims the gesture, so a `GlintCard` inside a `ScrollView` still scrolls normally.\n\n## When to use\n\n- A hero object the user is meant to pick up and look at: a pass, a ticket, a membership or achievement card.\n- One or two cards per screen, where the reflection adds a sense of material.\n\n## When not to use\n\n- Long lists of cards. Every live card runs its own sensor; set `reduceEffects` or pass `active={false}` to cards that are off-screen.\n- Cards whose main job is navigation. Pair the glint with a press response (see Press Jelly) instead of relying on it as feedback.\n\n## Notes\n\n- With `source=\"tilt\"` or `\"both\"` the card registers the gravity sensor while mounted and active. Unmounting it, or setting `active={false}`, unregisters the sensor.\n- The neutral pose recentres to however the user is holding the device, so the light always starts at rest.\n- Content is rendered beneath the glint. On dark surfaces the glint only brightens; on light surfaces (where white light cannot brighten white) it shades the card by up to 7 % away from the light instead, so check text contrast against a slightly darker card.\n- Tilt changes too small to see are ignored, so a phone lying still does not redraw the card.\n"
  },
  "docs": "Prefer `npx kinetik-ui add glint-card`: it installs dependencies with expo install, so versions match your Expo SDK."
}
