{
  "skill_name": "swiftui-animation",
  "evals": [
    {
      "id": 0,
      "name": "carousel-scroll-transition",
      "prompt": "Horizontal destination carousel in SwiftUI (iOS 17+). Right now we use a GeometryReader in every card plus an @State offset dictionary to scale and tilt cards toward the edges, and scrolling stutters on older iPhones. Rewrite it properly, and make sure it is fine for people who turn on Reduce Motion.",
      "expected_output": "Replaces the GeometryReader and state with .scrollTransition(axis: .horizontal) using ScrollTransitionPhase: scale and rotation driven by phase.value or isIdentity, no change in the identity phase, effects limited to visual effects. Optionally uses .scrollTargetBehavior(.viewAligned) with scrollTargetLayout. Reads accessibilityReduceMotion and keeps only opacity or passes .identity configuration, dropping rotation and scale. Notes the closure is Sendable so values are captured outside.",
      "files": [],
      "expectations": [
        "Uses .scrollTransition instead of GeometryReader plus @State offsets",
        "Drives the effect from ScrollTransitionPhase (value or isIdentity) and leaves the identity phase unchanged",
        "Uses the horizontal axis for the carousel",
        "Respects accessibilityReduceMotion by dropping rotation/scale or using the .identity configuration",
        "Explains why it is cheaper: visual effects do not change layout or invalidate state every frame"
      ]
    },
    {
      "id": 1,
      "name": "parallax-header",
      "prompt": "I want the classic stretchy parallax header image at the top of a ScrollView in SwiftUI: when you pull down it stretches, when you scroll up it moves at half speed. iOS 17 minimum. What's the modern way?",
      "expected_output": "Uses .visualEffect with the GeometryProxy, reading proxy.frame(in: .scrollView).minY, applying offset for half-speed movement and scale for the stretch, without GeometryReader-driven @State. Explains the closure is @Sendable and only visual effects (offset, scale, blur, opacity) are available, not layout. Mentions disabling parallax under Reduce Motion.",
      "files": [],
      "expectations": [
        "Uses .visualEffect with the GeometryProxy",
        "Reads the position in the scroll view coordinate space (.scrollView)",
        "Applies offset and scale effects rather than changing frame or padding",
        "Mentions turning parallax off when accessibilityReduceMotion is on",
        "Does not rely on GeometryReader plus @State updates on every scroll"
      ]
    },
    {
      "id": 2,
      "name": "button-feedback-curve",
      "prompt": "Code review: Button { withAnimation(.easeInOut(duration: 1.0)) { isLiked.toggle() } } label: { Image(systemName: isLiked ? \"heart.fill\" : \"heart\").scaleEffect(isLiked ? 1.3 : 1).animation(.linear) }. Also there's a DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { showToast = true } } after it. Fix it.",
      "expected_output": "Replaces the slow ease and bare .animation(.linear) with a spring (.snappy or .bouncy) via withAnimation or .animation(_:value:) or .animation(_:body:), uses .contentTransition(.symbolEffect(.replace)) or a symbol effect for the heart swap, replaces DispatchQueue delay with Animation.delay, and respects Reduce Motion.",
      "files": [],
      "expectations": [
        "Removes the bare .animation(.linear) without a value",
        "Uses a spring curve such as .snappy or .bouncy for the tap feedback",
        "Uses a symbol content transition or symbol effect for the heart change",
        "Replaces DispatchQueue.main.asyncAfter with Animation.delay or an equivalent animation-based delay",
        "Mentions accessibilityReduceMotion"
      ]
    }
  ]
}
